Add variants to sale view (#1420)

* Update schema and biuld types for sale per variant

* Create variant search module and generate types for it

* Add listing component for sale view

* Create dialog for variant assignment

* Expand sale page with vairnats

* Add new sale fixtures

* Add transaltions for variants on sale view

* Update snapshot

* Refactor sales dialogs and tables, move styles and ittl to local files

* Rework search dialog. Create item/subitem selectable table for variants, update spapshot

* Adjust table columns width

* Standardize the tables

* Unify messages

* Drop whole variant object in favor of just ids, simplify filtring functions

* Update snapshots

Co-authored-by: Jakub Majorek <majorek.jakub@gmail.com>
This commit is contained in:
Łukasz Szewczyk 2021-09-30 14:51:13 +02:00 committed by GitHub
parent 2407ae6f76
commit ec230e55c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 2006 additions and 392 deletions

View file

@ -492,17 +492,9 @@
"context": "number of products",
"string": "Products ({quantity})"
},
"saleDetailsUnassignCategory": {
"context": "unassign category from sale, button",
"string": "Unassign"
},
"saleDetailsUnassignCollection": {
"context": "unassign collection from sale, button",
"string": "Unassign"
},
"saleDetailsUnassignProduct": {
"context": "unassign product from sale, button",
"string": "Unassign"
"saleDetailsPageVariantsQuantity": {
"context": "number of variants",
"string": "Variants ({quantity})"
},
"set availability date": {
"context": "product availability date label",
@ -1889,44 +1881,69 @@
"context": "placeholder",
"string": "Search by value name, etc..."
},
"src_dot_components_dot_AssignCategoryDialog_dot_3125506097": {
"src_dot_components_dot_AssignCategoryDialog_dot_assignCategoryDialogHeader": {
"context": "dialog header",
"string": "Assign Category"
},
"src_dot_components_dot_AssignCategoryDialog_dot_3690273268": {
"string": "Search by category name, etc..."
},
"src_dot_components_dot_AssignCategoryDialog_dot_3841025483": {
"src_dot_components_dot_AssignCategoryDialog_dot_assignCategoryDialogLabel": {
"context": "dialog header",
"string": "Search Category"
},
"src_dot_components_dot_AssignCollectionDialog_dot_2605414502": {
"string": "Search by collection name, etc..."
"src_dot_components_dot_AssignCategoryDialog_dot_assignCategoryDialogPlaceholder": {
"context": "dialog search placeholder",
"string": "Search by category name, etc..."
},
"src_dot_components_dot_AssignCollectionDialog_dot_3992923611": {
"src_dot_components_dot_AssignCollectionDialog_dot_assignCollectionDialogHeader": {
"context": "dialog header",
"string": "Assign Collection"
},
"src_dot_components_dot_AssignCollectionDialog_dot_4057224233": {
"src_dot_components_dot_AssignCollectionDialog_dot_assignCollectionDialogLabel": {
"context": "dialog header",
"string": "Search Collection"
},
"src_dot_components_dot_AssignContainerDialog_dot_1731102929": {
"src_dot_components_dot_AssignCollectionDialog_dot_assignCollectionDialogPlaceholder": {
"context": "dialog search placeholder",
"string": "Search by collection name, etc..."
},
"src_dot_components_dot_AssignContainerDialog_dot_assignContainerDialogButton": {
"context": "button",
"string": "Assign"
},
"src_dot_components_dot_AssignProductDialog_dot_2100305525": {
"src_dot_components_dot_AssignProductDialog_dot_assignProductDialogButton": {
"context": "button",
"string": "Assign products"
"string": "Assign"
},
"src_dot_components_dot_AssignProductDialog_dot_2336947364": {
"string": "Search by product name, attribute, product type etc..."
},
"src_dot_components_dot_AssignProductDialog_dot_2850255786": {
"src_dot_components_dot_AssignProductDialog_dot_assignProductDialogContent": {
"string": "Search Products"
},
"src_dot_components_dot_AssignProductDialog_dot_649693468": {
"src_dot_components_dot_AssignProductDialog_dot_assignProductDialogSearch": {
"string": "Search by product name, attribute, product type etc..."
},
"src_dot_components_dot_AssignProductDialog_dot_assignVariantDialogHeader": {
"context": "dialog header",
"string": "Assign Product"
},
"src_dot_components_dot_AssignVariantDialog_dot_3284796469": {
"string": "No products available in order channel matching given query"
},
"src_dot_components_dot_AssignVariantDialog_dot_assignVariantDialogButton": {
"context": "button",
"string": "Assign"
},
"src_dot_components_dot_AssignVariantDialog_dot_assignVariantDialogContent": {
"string": "Search Variants"
},
"src_dot_components_dot_AssignVariantDialog_dot_assignVariantDialogHeader": {
"context": "dialog header",
"string": "Assign Variant"
},
"src_dot_components_dot_AssignVariantDialog_dot_assignVariantDialogSKU": {
"context": "variant sku",
"string": "SKU {sku}"
},
"src_dot_components_dot_AssignVariantDialog_dot_assignVariantDialogSearch": {
"string": "Search by product name, attribute, product type etc..."
},
"src_dot_components_dot_AttributeUnassignDialog_dot_2037985699": {
"string": "Are you sure you want to unassign {attributeName} from {itemTypeName}?"
},
@ -2835,42 +2852,46 @@
"src_dot_discounts": {
"string": "Discounts"
},
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_1567318211": {
"string": "Category name"
},
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_1681512341": {
"context": "section header",
"string": "Eligible Categories"
},
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_2054128296": {
"string": "No categories found"
},
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_2968663655": {
"context": "number of products",
"string": "Products"
},
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_3973677075": {
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_discountCategoriesButton": {
"context": "button",
"string": "Assign categories"
},
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_1035511604": {
"context": "button",
"string": "Assign collections"
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_discountCategoriesHeader": {
"context": "section header",
"string": "Eligible Categories"
},
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_2137803833": {
"string": "No collections found"
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_discountCategoriesNotFound": {
"context": "no categories",
"string": "No categories found"
},
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_2968663655": {
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_discountCategoriesTableProductHeader": {
"context": "table head",
"string": "Category Name"
},
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_discountCategoriesTableProductNumber": {
"context": "number of products",
"string": "Products"
},
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_3011396316": {
"string": "Collection name"
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_discountCollectionsButton": {
"context": "button",
"string": "Assign collections"
},
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_452750900": {
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_discountCollectionsHeader": {
"context": "section header",
"string": "Eligible Collections"
},
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_discountCollectionsNotFound": {
"context": "no collections",
"string": "No collections found"
},
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_discountCollectionsTableProductHeader": {
"context": "table head",
"string": "Collection Name"
},
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_discountCollectionsTableProductNumber": {
"context": "number of products",
"string": "Products"
},
"src_dot_discounts_dot_components_dot_DiscountCountrySelectDialog_dot_1585396479": {
"context": "dialog header",
"string": "Assign Countries"
@ -2902,26 +2923,53 @@
"context": "time during discount is active, header",
"string": "Active Dates"
},
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_1657559629": {
"string": "No products found"
},
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_2100305525": {
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_discountProductsButton": {
"context": "button",
"string": "Assign products"
},
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_2697405188": {
"string": "Product Name"
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_discountProductsHeader": {
"context": "section header",
"string": "Eligible Products"
},
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_3326160357": {
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_discountProductsNotFound": {
"context": "no products",
"string": "No products found"
},
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_discountProductsTableAvailabilityHeader": {
"context": "product availability",
"string": "Availability"
},
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_4257289053": {
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_discountProductsTableProductHeader": {
"context": "table head",
"string": "Product Name"
},
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_discountProductsTableTypeHeader": {
"context": "product type",
"string": "Product Type"
},
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_919175218": {
"src_dot_discounts_dot_components_dot_DiscountVariants_dot_discountVariantsButton": {
"context": "button",
"string": "Assign variants"
},
"src_dot_discounts_dot_components_dot_DiscountVariants_dot_discountVariantsHeader": {
"context": "section header",
"string": "Eligible Products"
"string": "Eligible Variants"
},
"src_dot_discounts_dot_components_dot_DiscountVariants_dot_discountVariantsNotFound": {
"context": "no variants",
"string": "No variants found"
},
"src_dot_discounts_dot_components_dot_DiscountVariants_dot_discountVariantsTableProductHeader": {
"context": "table head",
"string": "Product Name"
},
"src_dot_discounts_dot_components_dot_DiscountVariants_dot_discountVariantsTableTypeHeader": {
"context": "table head",
"string": "Product Type"
},
"src_dot_discounts_dot_components_dot_DiscountVariants_dot_discountVariantsTableVariantHeader": {
"context": "table head",
"string": "Variant Name"
},
"src_dot_discounts_dot_components_dot_SaleCreatePage_dot_3866518732": {
"context": "page header",
@ -3288,44 +3336,74 @@
"src_dot_discounts_dot_views_dot_SaleCreate_dot_480188715": {
"string": "Manage Sales Channel Availability"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_1457489953": {
"context": "dialog content",
"string": "Are you sure you want to delete {saleName}?"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_1827854264": {
"context": "dialog header",
"string": "Unassign Categories From Sale"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_1952217501": {
"context": "dialog header",
"string": "Unassign Collections From Sale"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_2353723060": {
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to unassign this category?} other{Are you sure you want to unassign {displayQuantity} categories?}}"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_2534378844": {
"string": "Removed sale"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_3215481647": {
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to unassign this product?} other{Are you sure you want to unassign {displayQuantity} products?}}"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_3395246518": {
"context": "dialog header",
"string": "Unassign Products From Sale"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_3823295269": {
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsChannelAvailabilityDialogHeader": {
"context": "channel availability dialog header",
"string": "Manage Channel Availability"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_506030254": {
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsSaleDelate": {
"context": "sale Details delete button",
"string": "Removed sale"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsSaleDeleteDialog": {
"context": "dialog content",
"string": "Removed sale"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsSaleDeleteDialogHeader": {
"context": "dialog header",
"string": "Delete Sale"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_767268203": {
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignCategory": {
"context": "unassign category from sale, button",
"string": "Unassign"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignCategoryDialog": {
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to unassign this category?} other{Are you sure you want to unassign {displayQuantity} categories?}}"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignCategoryDialogHeader": {
"context": "dialog header",
"string": "Unassign Categories From Sale"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignCollection": {
"context": "unassign collection from sale, button",
"string": "Unassign"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignCollectionDialog": {
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to unassign this collection?} other{Are you sure you want to unassign {displayQuantity} collections?}}"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignCollectionDialogHeader": {
"context": "dialog header",
"string": "Unassign Collection From Sale"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignDialogDelete": {
"context": "dialog content",
"string": "Are you sure you want to delete {saleName}?"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignProduct": {
"context": "unassign product from sale, button",
"string": "Unassign"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignProductDialog": {
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to unassign this product?} other{Are you sure you want to unassign {displayQuantity} products?}}"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignProductDialogHeader": {
"context": "dialog header",
"string": "Unassign Product From Sale"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignVariant": {
"context": "unassign variant from sale, button",
"string": "Unassign"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignVariantDialog": {
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to unassign this variant?} other{Are you sure you want to unassign {displayQuantity} variants?}}"
},
"src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsUnassignVariantDialogHeader": {
"context": "dialog header",
"string": "Unassign Variant From Sale"
},
"src_dot_discounts_dot_views_dot_SaleList_dot_2809303671": {
"context": "dialog header",
"string": "Delete Sales"

View file

@ -802,6 +802,7 @@ input CatalogueInput {
products: [ID]
categories: [ID]
collections: [ID]
variants: [ID]
}
type Category implements Node & ObjectWithMetadata {
@ -5905,6 +5906,7 @@ type Sale implements Node & ObjectWithMetadata {
categories(before: String, after: String, first: Int, last: Int): CategoryCountableConnection
collections(before: String, after: String, first: Int, last: Int): CollectionCountableConnection
products(before: String, after: String, first: Int, last: Int): ProductCountableConnection
variants(before: String, after: String, first: Int, last: Int): ProductVariantCountableConnection
translation(languageCode: LanguageCodeEnum!): SaleTranslation
channelListings: [SaleChannelListing!]
discountValue: Float
@ -5982,6 +5984,7 @@ input SaleInput {
type: DiscountValueTypeEnum
value: PositiveDecimal
products: [ID]
variants: [ID]
categories: [ID]
collections: [ID]
startDate: DateTime
@ -7297,4 +7300,4 @@ union _Entity = App | Address | User | Group | ProductVariant | Product | Produc
type _Service {
sdl: String
}
}

View file

@ -358,7 +358,7 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
variables: {
...paginationState,
collectionId: id,
productIds: products.map(product => product.id)
productIds: products
}
})
}

View file

@ -5,6 +5,7 @@ import { useIntl } from "react-intl";
import AssignContainerDialog, {
AssignContainerDialogProps
} from "../AssignContainerDialog";
import { messages } from "./messages";
interface AssignCategoryDialogProps
extends Omit<AssignContainerDialogProps, "containers" | "title" | "search"> {
@ -21,17 +22,12 @@ const AssignCategoryDialog: React.FC<AssignCategoryDialogProps> = ({
<AssignContainerDialog
containers={categories}
search={{
label: intl.formatMessage({
defaultMessage: "Search Category"
}),
placeholder: intl.formatMessage({
defaultMessage: "Search by category name, etc..."
})
label: intl.formatMessage(messages.assignCategoryDialogLabel),
placeholder: intl.formatMessage(
messages.assignCategoryDialogPlaceholder
)
}}
title={intl.formatMessage({
defaultMessage: "Assign Category",
description: "dialog header"
})}
title={intl.formatMessage(messages.assignCategoryDialogHeader)}
{...rest}
/>
);

View file

@ -0,0 +1,16 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
assignCategoryDialogLabel: {
defaultMessage: "Search Category",
description: "dialog header"
},
assignCategoryDialogPlaceholder: {
defaultMessage: "Search by category name, etc...",
description: "dialog search placeholder"
},
assignCategoryDialogHeader: {
defaultMessage: "Assign Category",
description: "dialog header"
}
});

View file

@ -5,6 +5,7 @@ import { useIntl } from "react-intl";
import AssignContainerDialog, {
AssignContainerDialogProps
} from "../AssignContainerDialog";
import { messages } from "./messages";
interface AssignCollectionDialogProps
extends Omit<AssignContainerDialogProps, "containers" | "title" | "search"> {
@ -21,17 +22,12 @@ const AssignCollectionDialog: React.FC<AssignCollectionDialogProps> = ({
<AssignContainerDialog
containers={collections}
search={{
label: intl.formatMessage({
defaultMessage: "Search Collection"
}),
placeholder: intl.formatMessage({
defaultMessage: "Search by collection name, etc..."
})
label: intl.formatMessage(messages.assignCollectionDialogLabel),
placeholder: intl.formatMessage(
messages.assignCollectionDialogPlaceholder
)
}}
title={intl.formatMessage({
defaultMessage: "Assign Collection",
description: "dialog header"
})}
title={intl.formatMessage(messages.assignCollectionDialogHeader)}
{...rest}
/>
);

View file

@ -0,0 +1,16 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
assignCollectionDialogLabel: {
defaultMessage: "Search Collection",
description: "dialog header"
},
assignCollectionDialogPlaceholder: {
defaultMessage: "Search by collection name, etc...",
description: "dialog search placeholder"
},
assignCollectionDialogHeader: {
defaultMessage: "Assign Collection",
description: "dialog header"
}
});

View file

@ -13,49 +13,33 @@ import {
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui";
import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle";
import { FetchMoreProps, Node } from "@saleor/types";
import { DialogProps, FetchMoreProps, Node } from "@saleor/types";
import React from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import { FormattedMessage } from "react-intl";
import Checkbox from "../Checkbox";
import ConfirmButton, { ConfirmButtonTransitionState } from "../ConfirmButton";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface FormData {
export interface AssignContainerDialogFormData {
containers: string[];
query: string;
}
const useStyles = makeStyles(
{
avatar: {
"&:first-child": {
paddingLeft: 0
}
},
checkboxCell: {
paddingLeft: 0
},
wideCell: {
width: "100%"
}
},
{ name: "AssignContainerDialog" }
);
interface Container extends Node {
name: string;
}
export interface AssignContainerDialogProps extends FetchMoreProps {
export interface AssignContainerDialogProps
extends FetchMoreProps,
DialogProps {
confirmButtonState: ConfirmButtonTransitionState;
containers: Container[];
loading: boolean;
open: boolean;
search: Record<"label" | "placeholder", string>;
title: string;
onClose: () => void;
onFetch: (value: string) => void;
onSubmit: (data: string[]) => void;
}
@ -188,7 +172,7 @@ const AssignContainerDialog: React.FC<AssignContainerDialogProps> = props => {
type="submit"
onClick={handleSubmit}
>
<FormattedMessage defaultMessage="Assign" description="button" />
<FormattedMessage {...messages.assignContainerDialogButton} />
</ConfirmButton>
</DialogActions>
</Dialog>

View file

@ -0,0 +1,8 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
assignContainerDialogButton: {
defaultMessage: "Assign",
description: "button"
}
});

View file

@ -0,0 +1,18 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
{
avatar: {
"&:first-child": {
paddingLeft: 0
}
},
checkboxCell: {
paddingLeft: 0
},
wideCell: {
width: "100%"
}
},
{ name: "AssignContainerDialog" }
);

View file

@ -17,65 +17,43 @@ import ResponsiveTable from "@saleor/components/ResponsiveTable";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui";
import { maybe } from "@saleor/misc";
import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts";
import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle";
import { FetchMoreProps } from "@saleor/types";
import { DialogProps, FetchMoreProps } from "@saleor/types";
import React from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import { FormattedMessage, useIntl } from "react-intl";
import Checkbox from "../Checkbox";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface FormData {
export interface AssignProductDialogFormData {
products: SearchProducts_search_edges_node[];
query: string;
}
const useStyles = makeStyles(
{
avatar: {
"&&:first-child": {
paddingLeft: 0
},
width: 72
},
checkboxCell: {
paddingLeft: 0,
width: 88
},
colName: {
paddingLeft: 0
}
},
{ name: "AssignProductDialog" }
);
export interface AssignProductDialogProps extends FetchMoreProps {
export interface AssignProductDialogProps extends FetchMoreProps, DialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
products: SearchProducts_search_edges_node[];
loading: boolean;
onClose: () => void;
onFetch: (value: string) => void;
onSubmit: (data: SearchProducts_search_edges_node[]) => void;
onSubmit: (data: string[]) => void;
}
function handleProductAssign(
product: SearchProducts_search_edges_node,
productID: string,
isSelected: boolean,
selectedProducts: SearchProducts_search_edges_node[],
setSelectedProducts: (data: SearchProducts_search_edges_node[]) => void
selectedProducts: string[],
setSelectedProducts: (data: string[]) => void
) {
if (isSelected) {
setSelectedProducts(
selectedProducts.filter(
selectedProduct => selectedProduct.id !== product.id
)
selectedProducts.filter(selectedProduct => selectedProduct !== productID)
);
} else {
setSelectedProducts([...selectedProducts, product]);
setSelectedProducts([...selectedProducts, productID]);
}
}
@ -98,9 +76,7 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedProducts, setSelectedProducts] = React.useState<
SearchProducts_search_edges_node[]
>([]);
const [selectedProducts, setSelectedProducts] = React.useState<string[]>([]);
const handleSubmit = () => onSubmit(selectedProducts);
@ -113,23 +89,15 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
maxWidth="sm"
>
<DialogTitle>
<FormattedMessage
defaultMessage="Assign Product"
description="dialog header"
/>
<FormattedMessage {...messages.assignVariantDialogHeader} />
</DialogTitle>
<DialogContent className={scrollableDialogClasses.topArea}>
<TextField
name="query"
value={query}
onChange={onQueryChange}
label={intl.formatMessage({
defaultMessage: "Search Products"
})}
placeholder={intl.formatMessage({
defaultMessage:
"Search by product name, attribute, product type etc..."
})}
label={intl.formatMessage(messages.assignProductDialogSearch)}
placeholder={intl.formatMessage(messages.assignProductDialogContent)}
fullWidth
InputProps={{
autoComplete: "off",
@ -158,7 +126,7 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
{products &&
products.map(product => {
const isSelected = selectedProducts.some(
selectedProduct => selectedProduct.id === product.id
selectedProduct => selectedProduct === product.id
);
return (
@ -181,7 +149,7 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
checked={isSelected}
onChange={() =>
handleProductAssign(
product,
product.id,
isSelected,
selectedProducts,
setSelectedProducts
@ -208,10 +176,7 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
type="submit"
onClick={handleSubmit}
>
<FormattedMessage
defaultMessage="Assign products"
description="button"
/>
<FormattedMessage {...messages.assignProductDialogButton} />
</ConfirmButton>
</DialogActions>
</Dialog>

View file

@ -0,0 +1,18 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
assignVariantDialogHeader: {
defaultMessage: "Assign Product",
description: "dialog header"
},
assignProductDialogButton: {
defaultMessage: "Assign",
description: "button"
},
assignProductDialogContent: {
defaultMessage: "Search Products"
},
assignProductDialogSearch: {
defaultMessage: "Search by product name, attribute, product type etc..."
}
});

View file

@ -0,0 +1,20 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
{
avatar: {
"&&:first-child": {
paddingLeft: 0
},
width: 72
},
checkboxCell: {
paddingLeft: 0,
width: 88
},
colName: {
paddingLeft: 0
}
},
{ name: "AssignProductDialog" }
);

View file

@ -0,0 +1,298 @@
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TableBody,
TableCell,
TableRow,
TextField
} from "@material-ui/core";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import Money from "@saleor/components/Money";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl";
import { maybe, renderCollection } from "@saleor/misc";
import {
getById,
getByUnmatchingId
} from "@saleor/orders/components/OrderReturnPage/utils";
import {
SearchProducts_search_edges_node,
SearchProducts_search_edges_node_variants
} from "@saleor/searches/types/SearchProducts";
import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle";
import { DialogProps, FetchMoreProps } from "@saleor/types";
import React from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import { FormattedMessage, useIntl } from "react-intl";
import Checkbox from "../Checkbox";
import { messages } from "./messages";
import { useStyles } from "./styles";
type SetVariantsAction = (
data: SearchProducts_search_edges_node_variants[]
) => void;
export interface AssignVariantDialogFormData {
products: SearchProducts_search_edges_node[];
query: string;
}
export interface AssignVariantDialogProps extends FetchMoreProps, DialogProps {
confirmButtonState: ConfirmButtonTransitionState;
products: SearchProducts_search_edges_node[];
loading: boolean;
onFetch: (value: string) => void;
onSubmit: (data: SearchProducts_search_edges_node_variants[]) => void;
}
function isVariantSelected(
variant: SearchProducts_search_edges_node_variants,
selectedVariantsToProductsMap: SearchProducts_search_edges_node_variants[]
): boolean {
return !!selectedVariantsToProductsMap.find(getById(variant.id));
}
const handleProductAssign = (
product: SearchProducts_search_edges_node,
productIndex: number,
productsWithAllVariantsSelected: boolean[],
variants: SearchProducts_search_edges_node_variants[],
setVariants: SetVariantsAction
) =>
productsWithAllVariantsSelected[productIndex]
? setVariants(
variants.filter(
selectedVariant => !product.variants.find(getById(selectedVariant.id))
)
)
: setVariants([
...variants,
...product.variants.filter(
productVariant => !variants.find(getById(productVariant.id))
)
]);
const handleVariantAssign = (
variant: SearchProducts_search_edges_node_variants,
variantIndex: number,
productIndex: number,
variants: SearchProducts_search_edges_node_variants[],
selectedVariantsToProductsMap: boolean[][],
setVariants: SetVariantsAction
) =>
selectedVariantsToProductsMap[productIndex][variantIndex]
? setVariants(variants.filter(getByUnmatchingId(variant.id)))
: setVariants([...variants, variant]);
function hasAllVariantsSelected(
productVariants: SearchProducts_search_edges_node_variants[],
selectedVariantsToProductsMap: SearchProducts_search_edges_node_variants[]
): boolean {
return productVariants.reduce(
(acc, productVariant) =>
acc && !!selectedVariantsToProductsMap.find(getById(productVariant.id)),
true
);
}
const scrollableTargetId = "assignVariantScrollableDialog";
const AssignVariantDialog: React.FC<AssignVariantDialogProps> = props => {
const {
confirmButtonState,
hasMore,
open,
loading,
products,
onClose,
onFetch,
onFetchMore,
onSubmit
} = props;
const classes = useStyles(props);
const scrollableDialogClasses = useScrollableDialogStyle({});
const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch);
const [variants, setVariants] = React.useState<
SearchProducts_search_edges_node_variants[]
>([]);
const productChoices =
products?.filter(product => product?.variants?.length > 0) || [];
const selectedVariantsToProductsMap = productChoices
? productChoices.map(product =>
product.variants.map(variant => isVariantSelected(variant, variants))
)
: [];
const productsWithAllVariantsSelected = productChoices
? productChoices.map(product =>
hasAllVariantsSelected(product.variants, variants)
)
: [];
const handleSubmit = () => onSubmit(variants);
return (
<Dialog
onClose={onClose}
open={open}
classes={{ paper: scrollableDialogClasses.dialog }}
fullWidth
maxWidth="sm"
>
<DialogTitle>
<FormattedMessage {...messages.assignVariantDialogHeader} />
</DialogTitle>
<DialogContent className={scrollableDialogClasses.topArea}>
<TextField
name="query"
value={query}
onChange={onQueryChange}
label={intl.formatMessage(messages.assignVariantDialogSearch)}
placeholder={intl.formatMessage(messages.assignVariantDialogContent)}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
/>
</DialogContent>
<DialogContent
className={scrollableDialogClasses.scrollArea}
id={scrollableTargetId}
>
<InfiniteScroll
dataLength={variants?.length}
next={onFetchMore}
hasMore={hasMore}
scrollThreshold="100px"
loader={
<div className={scrollableDialogClasses.loadMoreLoaderContainer}>
<CircularProgress size={16} />
</div>
}
scrollableTarget={scrollableTargetId}
>
<ResponsiveTable key="table">
<TableBody>
{renderCollection(
products,
(product, productIndex) => (
<React.Fragment key={product ? product.id : "skeleton"}>
<TableRow>
<TableCell
padding="checkbox"
className={classes.productCheckboxCell}
>
<Checkbox
checked={
productsWithAllVariantsSelected[productIndex]
}
disabled={loading}
onChange={() =>
handleProductAssign(
product,
productIndex,
productsWithAllVariantsSelected,
variants,
setVariants
)
}
/>
</TableCell>
<TableCellAvatar
className={classes.avatar}
thumbnail={maybe(() => product.thumbnail.url)}
/>
<TableCell className={classes.colName} colSpan={2}>
{maybe(() => product.name)}
</TableCell>
</TableRow>
{maybe(() => product.variants, []).map(
(variant, variantIndex) => (
<TableRow key={variant.id}>
<TableCell />
<TableCell className={classes.colVariantCheckbox}>
<Checkbox
className={classes.variantCheckbox}
checked={
selectedVariantsToProductsMap[productIndex][
variantIndex
]
}
disabled={loading}
onChange={() =>
handleVariantAssign(
variant,
variantIndex,
productIndex,
variants,
selectedVariantsToProductsMap,
setVariants
)
}
/>
</TableCell>
<TableCell className={classes.colName}>
<div>{variant.name}</div>
<div className={classes.grayText}>
<FormattedMessage
{...messages.assignVariantDialogSKU}
values={{
sku: variant.sku
}}
/>
</div>
</TableCell>
<TableCell className={classes.textRight}>
{variant?.channelListings[0]?.price && (
<Money money={variant.channelListings[0].price} />
)}
</TableCell>
</TableRow>
)
)}
</React.Fragment>
),
() => (
<TableRow>
<TableCell colSpan={4}>
<FormattedMessage defaultMessage="No products available in order channel matching given query" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</ResponsiveTable>
</InfiniteScroll>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
data-test="submit"
transitionState={confirmButtonState}
color="primary"
variant="contained"
type="submit"
onClick={handleSubmit}
>
<FormattedMessage {...messages.assignVariantDialogButton} />
</ConfirmButton>
</DialogActions>
</Dialog>
);
};
AssignVariantDialog.displayName = "AssignVariantDialog";
export default AssignVariantDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./AssignVariantDialog";
export * from "./AssignVariantDialog";

View file

@ -0,0 +1,22 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
assignVariantDialogHeader: {
defaultMessage: "Assign Variant",
description: "dialog header"
},
assignVariantDialogButton: {
defaultMessage: "Assign",
description: "button"
},
assignVariantDialogContent: {
defaultMessage: "Search Variants"
},
assignVariantDialogSearch: {
defaultMessage: "Search by product name, attribute, product type etc..."
},
assignVariantDialogSKU: {
defaultMessage: "SKU {sku}",
description: "variant sku"
}
});

View file

@ -0,0 +1,56 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
avatar: {
paddingLeft: 0,
width: 64
},
colName: {
paddingLeft: 0
},
colVariantCheckbox: {
padding: 0
},
content: {
overflowY: "scroll",
paddingTop: 0,
marginBottom: theme.spacing(3)
},
grayText: {
color: theme.palette.text.disabled
},
loadMoreLoaderContainer: {
alignItems: "center",
display: "flex",
height: theme.spacing(3),
justifyContent: "center",
marginTop: theme.spacing(3)
},
overflow: {
overflowY: "hidden"
},
topArea: {
overflowY: "hidden",
paddingBottom: theme.spacing(6),
margin: theme.spacing(0, 3, 3, 3)
},
productCheckboxCell: {
"&:first-child": {
paddingLeft: 0,
paddingRight: 0
}
},
textRight: {
textAlign: "right"
},
variantCheckbox: {
left: theme.spacing(),
position: "relative"
},
wideCell: {
width: "100%"
}
}),
{ name: "AssignVariantDialog" }
);

View file

@ -14,7 +14,6 @@ import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import { makeStyles } from "@saleor/macaw-ui";
import { mapEdgesToItems } from "@saleor/utils/maps";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -23,35 +22,14 @@ import { maybe, renderCollection } from "../../../misc";
import { ListActions, ListProps } from "../../../types";
import { SaleDetails_sale } from "../../types/SaleDetails";
import { VoucherDetails_voucher } from "../../types/VoucherDetails";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface DiscountCategoriesProps extends ListProps, ListActions {
discount: SaleDetails_sale | VoucherDetails_voucher;
onCategoryAssign: () => void;
onCategoryUnassign: (id: string) => void;
}
const useStyles = makeStyles(
{
colActions: {
"&:last-child": {
paddingRight: 0
},
width: 80
},
colName: {
width: "auto"
},
colProducts: {
textAlign: "right",
width: 140
},
tableRow: {
cursor: "pointer"
}
},
{ name: "DiscountCategories" }
);
const numberOfColumns = 4;
const DiscountCategories: React.FC<DiscountCategoriesProps> = props => {
@ -77,16 +55,10 @@ const DiscountCategories: React.FC<DiscountCategoriesProps> = props => {
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Eligible Categories",
description: "section header"
})}
title={intl.formatMessage(messages.discountCategoriesHeader)}
toolbar={
<Button color="primary" onClick={onCategoryAssign}>
<FormattedMessage
defaultMessage="Assign categories"
description="button"
/>
<FormattedMessage {...messages.discountCategoriesButton} />
</Button>
}
/>
@ -107,12 +79,13 @@ const DiscountCategories: React.FC<DiscountCategoriesProps> = props => {
>
<>
<TableCell className={classes.colName}>
<FormattedMessage defaultMessage="Category name" />
<FormattedMessage
{...messages.discountCategoriesTableProductHeader}
/>
</TableCell>
<TableCell className={classes.colProducts}>
<FormattedMessage
defaultMessage="Products"
description="number of products"
{...messages.discountCategoriesTableProductNumber}
/>
</TableCell>
<TableCell />
@ -179,7 +152,7 @@ const DiscountCategories: React.FC<DiscountCategoriesProps> = props => {
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No categories found" />
<FormattedMessage {...messages.discountCategoriesNotFound} />
</TableCell>
</TableRow>
)

View file

@ -0,0 +1,24 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
discountCategoriesHeader: {
defaultMessage: "Eligible Categories",
description: "section header"
},
discountCategoriesButton: {
defaultMessage: "Assign categories",
description: "button"
},
discountCategoriesTableProductHeader: {
defaultMessage: "Category Name",
description: "table head"
},
discountCategoriesTableProductNumber: {
defaultMessage: "Products",
description: "number of products"
},
discountCategoriesNotFound: {
defaultMessage: "No categories found",
description: "no categories"
}
});

View file

@ -0,0 +1,23 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
{
colActions: {
"&:last-child": {
paddingRight: 0
},
width: 80
},
colName: {
width: "auto"
},
colProducts: {
textAlign: "right",
width: 140
},
tableRow: {
cursor: "pointer"
}
},
{ name: "DiscountCategories" }
);

View file

@ -14,7 +14,6 @@ import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import { makeStyles } from "@saleor/macaw-ui";
import { mapEdgesToItems } from "@saleor/utils/maps";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -23,36 +22,14 @@ import { maybe, renderCollection } from "../../../misc";
import { ListActions, ListProps } from "../../../types";
import { SaleDetails_sale } from "../../types/SaleDetails";
import { VoucherDetails_voucher } from "../../types/VoucherDetails";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface DiscountCollectionsProps extends ListProps, ListActions {
discount: SaleDetails_sale | VoucherDetails_voucher;
onCollectionAssign: () => void;
onCollectionUnassign: (id: string) => void;
}
const useStyles = makeStyles(
{
colActions: {
"&:last-child": {
paddingRight: 0
},
width: 80
},
colName: {
width: "auto"
},
colProducts: {
textAlign: "right",
width: 140
},
tableRow: {
cursor: "pointer"
},
textRight: {}
},
{ name: "DiscountCollections" }
);
const numberOfColumns = 4;
const DiscountCollections: React.FC<DiscountCollectionsProps> = props => {
@ -79,16 +56,10 @@ const DiscountCollections: React.FC<DiscountCollectionsProps> = props => {
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Eligible Collections",
description: "section header"
})}
title={intl.formatMessage(messages.discountCollectionsHeader)}
toolbar={
<Button color="primary" onClick={onCollectionAssign}>
<FormattedMessage
defaultMessage="Assign collections"
description="button"
/>
<FormattedMessage {...messages.discountCollectionsButton} />
</Button>
}
/>
@ -108,12 +79,13 @@ const DiscountCollections: React.FC<DiscountCollectionsProps> = props => {
toolbar={toolbar}
>
<TableCell className={classes.colName}>
<FormattedMessage defaultMessage="Collection name" />
</TableCell>
<TableCell className={classes.textRight}>
<FormattedMessage
defaultMessage="Products"
description="number of products"
{...messages.discountCollectionsTableProductHeader}
/>
</TableCell>
<TableCell className={classes.colProducts}>
<FormattedMessage
{...messages.discountCollectionsTableProductNumber}
/>
</TableCell>
<TableCell />
@ -181,7 +153,7 @@ const DiscountCollections: React.FC<DiscountCollectionsProps> = props => {
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No collections found" />
<FormattedMessage {...messages.discountCollectionsNotFound} />
</TableCell>
</TableRow>
)

View file

@ -0,0 +1,24 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
discountCollectionsHeader: {
defaultMessage: "Eligible Collections",
description: "section header"
},
discountCollectionsButton: {
defaultMessage: "Assign collections",
description: "button"
},
discountCollectionsTableProductHeader: {
defaultMessage: "Collection Name",
description: "table head"
},
discountCollectionsTableProductNumber: {
defaultMessage: "Products",
description: "number of products"
},
discountCollectionsNotFound: {
defaultMessage: "No collections found",
description: "no collections"
}
});

View file

@ -0,0 +1,23 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
{
colActions: {
"&:last-child": {
paddingRight: 0
},
width: 80
},
colName: {
width: "auto"
},
colProducts: {
textAlign: "right",
width: 140
},
tableRow: {
cursor: "pointer"
}
},
{ name: "DiscountCollections" }
);

View file

@ -14,63 +14,32 @@ import Checkbox from "@saleor/components/Checkbox";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import { makeStyles } from "@saleor/macaw-ui";
import { mapEdgesToItems } from "@saleor/utils/maps";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc";
import { ListActions, ListProps } from "../../../types";
import { SaleDetails_sale } from "../../types/SaleDetails";
import { VoucherDetails_voucher } from "../../types/VoucherDetails";
import { SaleDetails_sale_products_edges_node } from "../../types/SaleDetails";
import { VoucherDetails_voucher_products_edges_node } from "../../types/VoucherDetails";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface SaleProductsProps extends ListProps, ListActions {
discount: SaleDetails_sale | VoucherDetails_voucher;
products:
| SaleDetails_sale_products_edges_node[]
| VoucherDetails_voucher_products_edges_node[];
channelsCount: number;
onProductAssign: () => void;
onProductUnassign: (id: string) => void;
}
const useStyles = makeStyles(
theme => ({
colActions: {
"&:last-child": {
paddingRight: 0
},
width: `calc(76px + ${theme.spacing(0.5)})`
},
colName: {
paddingLeft: 0,
width: "auto"
},
colNameLabel: {
marginLeft: `calc(${AVATAR_MARGIN}px + ${theme.spacing(3)})`
},
colPublished: {
width: 150
},
colType: {
width: 200
},
table: {
tableLayout: "fixed"
},
tableRow: {
cursor: "pointer"
}
}),
{ name: "DiscountProducts" }
);
const numberOfColumns = 5;
const DiscountProducts: React.FC<SaleProductsProps> = props => {
const {
channelsCount,
discount: sale,
products,
disabled,
pageInfo,
onRowClick,
@ -91,20 +60,14 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Eligible Products",
description: "section header"
})}
title={intl.formatMessage(messages.discountProductsHeader)}
toolbar={
<Button
color="primary"
onClick={onProductAssign}
data-test-id="assign-products"
>
<FormattedMessage
defaultMessage="Assign products"
description="button"
/>
<FormattedMessage {...messages.discountProductsButton} />
</Button>
}
/>
@ -120,22 +83,23 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={mapEdgesToItems(sale?.products)}
items={products}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colName}>
<span className={classes.colNameLabel}>
<FormattedMessage defaultMessage="Product Name" />
<span className={products?.length > 0 && classes.colNameLabel}>
<FormattedMessage
{...messages.discountProductsTableProductHeader}
/>
</span>
</TableCell>
<TableCell className={classes.colType}>
<FormattedMessage defaultMessage="Product Type" />
<FormattedMessage {...messages.discountProductsTableTypeHeader} />
</TableCell>
<TableCell className={classes.colPublished}>
<FormattedMessage
defaultMessage="Availability"
description="product availability"
{...messages.discountProductsTableAvailabilityHeader}
/>
</TableCell>
<TableCell className={classes.colActions} />
@ -155,7 +119,7 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
</TableFooter>
<TableBody>
{renderCollection(
mapEdgesToItems(sale?.products),
products,
product => {
const isSelected = product ? isChecked(product.id) : false;
@ -216,7 +180,7 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No products found" />
<FormattedMessage {...messages.discountProductsNotFound} />
</TableCell>
</TableRow>
)

View file

@ -0,0 +1,28 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
discountProductsHeader: {
defaultMessage: "Eligible Products",
description: "section header"
},
discountProductsButton: {
defaultMessage: "Assign products",
description: "button"
},
discountProductsTableProductHeader: {
defaultMessage: "Product Name",
description: "table head"
},
discountProductsTableTypeHeader: {
defaultMessage: "Product Type",
description: "product type"
},
discountProductsTableAvailabilityHeader: {
defaultMessage: "Availability",
description: "product availability"
},
discountProductsNotFound: {
defaultMessage: "No products found",
description: "no products"
}
});

View file

@ -0,0 +1,36 @@
import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar";
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
colActions: {
"&:last-child": {
paddingRight: 0
},
width: `calc(76px + ${theme.spacing(0.5)})`
},
colName: {
paddingLeft: 0,
width: "auto",
minWidth: 200
},
colNameLabel: {
marginLeft: `calc(${AVATAR_MARGIN}px + ${theme.spacing(3)})`
},
colPublished: {
width: "auto",
minWidth: 150
},
colType: {
width: "auto",
minWidth: 150
},
table: {
tableLayout: "fixed"
},
tableRow: {
cursor: "pointer"
}
}),
{ name: "DiscountProducts" }
);

View file

@ -0,0 +1,189 @@
import {
Button,
Card,
IconButton,
TableBody,
TableCell,
TableFooter,
TableRow
} from "@material-ui/core";
import DeleteIcon from "@material-ui/icons/Delete";
import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc";
import { ListActions, ListProps } from "../../../types";
import { SaleDetails_sale_variants_edges_node } from "../../types/SaleDetails";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface SaleVariantsProps
extends Omit<ListProps, "onRowClick">,
ListActions {
variants: SaleDetails_sale_variants_edges_node[] | null;
onVariantAssign: () => void;
onRowClick: (productId: string, variantId: string) => () => void;
onVariantUnassign: (id: string) => void;
}
const numberOfColumns = 5;
const DiscountVariants: React.FC<SaleVariantsProps> = props => {
const {
variants,
disabled,
pageInfo,
onRowClick,
onPreviousPage,
onVariantAssign,
onVariantUnassign,
onNextPage,
isChecked,
selected,
toggle,
toggleAll,
toolbar
} = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card>
<CardTitle
title={intl.formatMessage(messages.discountVariantsHeader)}
toolbar={
<Button
color="primary"
onClick={onVariantAssign}
data-test-id="assign-variant"
>
<FormattedMessage {...messages.discountVariantsButton} />
</Button>
}
/>
<ResponsiveTable>
<colgroup>
<col />
<col className={classes.colProductName} />
<col className={classes.colVariantName} />
<col className={classes.colType} />
<col className={classes.colActions} />
</colgroup>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={variants}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colProductName}>
<span className={variants?.length > 0 && classes.colNameLabel}>
<FormattedMessage
{...messages.discountVariantsTableProductHeader}
/>
</span>
</TableCell>
<TableCell className={classes.colVariantName}>
<FormattedMessage
{...messages.discountVariantsTableVariantHeader}
/>
</TableCell>
<TableCell className={classes.colType}>
<FormattedMessage
{...messages.discountVariantsTableProductHeader}
/>
</TableCell>
<TableCell className={classes.colActions} />
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
variants,
variant => {
const isSelected = variant ? isChecked(variant.id) : false;
return (
<TableRow
hover={!!variant}
key={variant ? variant.id : "skeleton"}
onClick={
variant && onRowClick(variant.product.id, variant.id)
}
className={classes.tableRow}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(variant.id)}
/>
</TableCell>
<TableCellAvatar
className={classes.colProductName}
thumbnail={maybe(() => variant.product.thumbnail.url)}
>
{maybe<React.ReactNode>(
() => variant.product.name,
<Skeleton />
)}
</TableCellAvatar>
<TableCell className={classes.colType}>
{maybe<React.ReactNode>(() => variant.name, <Skeleton />)}
</TableCell>
<TableCell className={classes.colType}>
{maybe<React.ReactNode>(
() => variant.product.productType.name,
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colActions}>
<IconButton
disabled={!variant || disabled}
onClick={event => {
event.stopPropagation();
onVariantUnassign(variant.id);
}}
>
<DeleteIcon color="primary" />
</IconButton>
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage {...messages.discountVariantsNotFound} />
</TableCell>
</TableRow>
)
)}
</TableBody>
</ResponsiveTable>
</Card>
);
};
DiscountVariants.displayName = "DiscountVariants";
export default DiscountVariants;

View file

@ -0,0 +1,2 @@
export { default } from "./DiscountVariants";
export * from "./DiscountVariants";

View file

@ -0,0 +1,28 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
discountVariantsHeader: {
defaultMessage: "Eligible Variants",
description: "section header"
},
discountVariantsButton: {
defaultMessage: "Assign variants",
description: "button"
},
discountVariantsTableProductHeader: {
defaultMessage: "Product Name",
description: "table head"
},
discountVariantsTableVariantHeader: {
defaultMessage: "Variant Name",
description: "table head"
},
discountVariantsTableTypeHeader: {
defaultMessage: "Product Type",
description: "table head"
},
discountVariantsNotFound: {
defaultMessage: "No variants found",
description: "no variants"
}
});

View file

@ -0,0 +1,36 @@
import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar";
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
colActions: {
"&:last-child": {
paddingRight: 0
},
width: `calc(76px + ${theme.spacing(0.5)})`
},
colProductName: {
paddingLeft: 0,
width: "auto",
minWidth: 200
},
colNameLabel: {
marginLeft: `calc(${AVATAR_MARGIN}px + ${theme.spacing(3)})`
},
colVariantName: {
width: "auto",
minWidth: 150
},
colType: {
width: "auto",
minWidth: 150
},
table: {
tableLayout: "fixed"
},
tableRow: {
cursor: "pointer"
}
}),
{ name: "DiscountVariants" }
);

View file

@ -14,6 +14,7 @@ import { DiscountErrorFragment } from "@saleor/fragments/types/DiscountErrorFrag
import { sectionNames } from "@saleor/intl";
import { Backlink } from "@saleor/macaw-ui";
import { validatePrice } from "@saleor/products/utils/validation";
import { mapEdgesToItems } from "@saleor/utils/maps";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react";
@ -30,6 +31,7 @@ import DiscountCategories from "../DiscountCategories";
import DiscountCollections from "../DiscountCollections";
import DiscountDates from "../DiscountDates";
import DiscountProducts from "../DiscountProducts";
import DiscountVariants from "../DiscountVariants";
import SaleInfo from "../SaleInfo";
import SaleSummary from "../SaleSummary";
import SaleType from "../SaleType";
@ -49,20 +51,27 @@ export interface SaleDetailsPageFormData extends MetadataFormData {
export enum SaleDetailsPageTab {
categories = "categories",
collections = "collections",
products = "products"
products = "products",
variants = "variants"
}
export function saleDetailsPageTab(tab: string): SaleDetailsPageTab {
return tab === SaleDetailsPageTab.products
? SaleDetailsPageTab.products
: tab === SaleDetailsPageTab.collections
? SaleDetailsPageTab.collections
: SaleDetailsPageTab.categories;
: tab === SaleDetailsPageTab.categories
? SaleDetailsPageTab.categories
: SaleDetailsPageTab.variants;
}
export interface SaleDetailsPageProps
extends Pick<ListProps, Exclude<keyof ListProps, "onRowClick">>,
TabListActions<
"categoryListToolbar" | "collectionListToolbar" | "productListToolbar"
| "categoryListToolbar"
| "collectionListToolbar"
| "productListToolbar"
| "variantListToolbar"
>,
ChannelProps {
activeTab: SaleDetailsPageTab;
@ -82,6 +91,9 @@ export interface SaleDetailsPageProps
onProductAssign: () => void;
onProductUnassign: (id: string) => void;
onProductClick: (id: string) => () => void;
onVariantAssign: () => void;
onVariantUnassign: (id: string) => void;
onVariantClick: (productId: string, variantId: string) => () => void;
onRemove: () => void;
onSubmit: (data: SaleDetailsPageFormData) => void;
onTabClick: (index: SaleDetailsPageTab) => void;
@ -92,6 +104,7 @@ export interface SaleDetailsPageProps
const CategoriesTab = Tab(SaleDetailsPageTab.categories);
const CollectionsTab = Tab(SaleDetailsPageTab.collections);
const ProductsTab = Tab(SaleDetailsPageTab.products);
const VariantsTab = Tab(SaleDetailsPageTab.variants);
const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
activeTab,
@ -120,9 +133,13 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
onProductAssign,
onProductUnassign,
onProductClick,
onVariantAssign,
onVariantUnassign,
onVariantClick,
categoryListToolbar,
collectionListToolbar,
productListToolbar,
variantListToolbar,
isChecked,
selected,
selectedChannelId,
@ -239,6 +256,25 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
}
)}
</ProductsTab>
<VariantsTab
testId="variants-tab"
isActive={activeTab === SaleDetailsPageTab.variants}
changeTab={onTabClick}
>
{intl.formatMessage(
{
defaultMessage: "Variants ({quantity})",
description: "number of variants",
id: "saleDetailsPageVariantsQuantity"
},
{
quantity: maybe(
() => sale.variants.totalCount.toString(),
"…"
)
}
)}
</VariantsTab>
</TabContainer>
<CardSpacer />
{activeTab === SaleDetailsPageTab.categories ? (
@ -273,7 +309,7 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
toggleAll={toggleAll}
toolbar={collectionListToolbar}
/>
) : (
) : activeTab === SaleDetailsPageTab.products ? (
<DiscountProducts
disabled={disabled}
onNextPage={onNextPage}
@ -282,7 +318,7 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
onProductUnassign={onProductUnassign}
onRowClick={onProductClick}
pageInfo={pageInfo}
discount={sale}
products={mapEdgesToItems(sale?.products)}
channelsCount={allChannelsCount}
isChecked={isChecked}
selected={selected}
@ -290,6 +326,22 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
toggleAll={toggleAll}
toolbar={productListToolbar}
/>
) : (
<DiscountVariants
disabled={disabled}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
onVariantAssign={onVariantAssign}
onVariantUnassign={onVariantUnassign}
onRowClick={onVariantClick}
pageInfo={pageInfo}
variants={mapEdgesToItems(sale?.variants)}
isChecked={isChecked}
selected={selected}
toggle={toggle}
toggleAll={toggleAll}
toolbar={variantListToolbar}
/>
)}
<CardSpacer />
<DiscountDates

View file

@ -20,6 +20,7 @@ import { DiscountErrorFragment } from "@saleor/fragments/types/DiscountErrorFrag
import { sectionNames } from "@saleor/intl";
import { Backlink } from "@saleor/macaw-ui";
import { validatePrice } from "@saleor/products/utils/validation";
import { mapEdgesToItems } from "@saleor/utils/maps";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react";
@ -353,7 +354,7 @@ const VoucherDetailsPage: React.FC<VoucherDetailsPageProps> = ({
onProductUnassign={onProductUnassign}
onRowClick={onProductClick}
pageInfo={pageInfo}
discount={voucher}
products={mapEdgesToItems(voucher.products)}
channelsCount={allChannelsCount}
isChecked={isChecked}
selected={selected}

View file

@ -424,6 +424,147 @@ export const sale: SaleDetails_sale = {
},
totalCount: 4
},
variants: {
edges: [
{
node: {
id: "UHJvZHVjdFZhcmlhbnQ6MzE0",
name: "XL",
product: {
id: "UHJvZHVjdDoxMTg=",
name: "White Hoodie",
thumbnail: {
url: placeholderImage,
__typename: "Image"
},
productType: {
id: "UHJvZHVjdFR5cGU6MTQ=",
name: "Top (clothing)",
__typename: "ProductType"
},
channelListings: [
{
isPublished: true,
publicationDate: "2020-01-01",
isAvailableForPurchase: true,
availableForPurchase: "2020-08-31",
visibleInListings: true,
channel: {
id: "Q2hhbm5lbDox",
name: "Channel-USD",
currencyCode: "USD",
__typename: "Channel"
},
__typename: "ProductChannelListing"
}
],
__typename: "Product"
},
__typename: "ProductVariant"
},
__typename: "ProductVariantCountableEdge"
},
{
node: {
id: "UHJvZHVjdFZhcmlhbnQ6Mjc4",
name: "L",
product: {
id: "UHJvZHVjdDoxMTE=",
name: "T-shirt",
thumbnail: {
url: placeholderImage,
__typename: "Image"
},
productType: {
id: "UHJvZHVjdFR5cGU6MTQ=",
name: "Top (clothing)",
__typename: "ProductType"
},
channelListings: [
{
isPublished: true,
publicationDate: "2020-01-01",
isAvailableForPurchase: true,
availableForPurchase: "2020-08-31",
visibleInListings: true,
channel: {
id: "Q2hhbm5lbDox",
name: "Channel-USD",
currencyCode: "USD",
__typename: "Channel"
},
__typename: "ProductChannelListing"
}
],
__typename: "Product"
},
__typename: "ProductVariant"
},
__typename: "ProductVariantCountableEdge"
},
{
node: {
id: "UHJvZHVjdFZhcmlhbnQ6MjUz",
name: "L",
product: {
id: "UHJvZHVjdDo4OQ==",
name: "Code Division T-shirt",
thumbnail: {
url: placeholderImage,
__typename: "Image"
},
productType: {
id: "UHJvZHVjdFR5cGU6MTQ=",
name: "Top (clothing)",
__typename: "ProductType"
},
channelListings: [
{
isPublished: true,
publicationDate: "2020-01-01",
isAvailableForPurchase: true,
availableForPurchase: "2020-08-31",
visibleInListings: true,
channel: {
id: "Q2hhbm5lbDox",
name: "Channel-USD",
currencyCode: "USD",
__typename: "Channel"
},
__typename: "ProductChannelListing"
},
{
isPublished: true,
publicationDate: "2020-01-01",
isAvailableForPurchase: true,
availableForPurchase: "2020-08-31",
visibleInListings: true,
channel: {
id: "Q2hhbm5lbDoy",
name: "Channel-PLN",
currencyCode: "PLN",
__typename: "Channel"
},
__typename: "ProductChannelListing"
}
],
__typename: "Product"
},
__typename: "ProductVariant"
},
__typename: "ProductVariantCountableEdge"
}
],
pageInfo: {
endCursor: "W251bGwsICIxMTgyMjM1OTEiXQ==",
hasNextPage: false,
hasPreviousPage: false,
startCursor: "W251bGwsICIxMDQwNDk0NiJd",
__typename: "PageInfo"
},
totalCount: 3,
__typename: "ProductVariantCountableConnection"
},
startDate: "2019-01-03",
type: "PERCENTAGE" as SaleType
};

View file

@ -43,6 +43,70 @@ export interface SaleCataloguesAdd_saleCataloguesAdd_sale_channelListings {
currency: string;
}
export interface SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node_product_thumbnail {
__typename: "Image";
url: string;
}
export interface SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node_product_productType {
__typename: "ProductType";
id: string;
name: string;
}
export interface SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node_product_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
currencyCode: string;
}
export interface SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node_product_channelListings {
__typename: "ProductChannelListing";
isPublished: boolean;
publicationDate: any | null;
isAvailableForPurchase: boolean | null;
availableForPurchase: any | null;
visibleInListings: boolean;
channel: SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node_product_channelListings_channel;
}
export interface SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node_product {
__typename: "Product";
id: string;
name: string;
thumbnail: SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node_product_thumbnail | null;
productType: SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node_product_productType;
channelListings: SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node_product_channelListings[] | null;
}
export interface SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node {
__typename: "ProductVariant";
id: string;
name: string;
product: SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node_product;
}
export interface SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges {
__typename: "ProductVariantCountableEdge";
node: SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges_node;
}
export interface SaleCataloguesAdd_saleCataloguesAdd_sale_variants_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SaleCataloguesAdd_saleCataloguesAdd_sale_variants {
__typename: "ProductVariantCountableConnection";
edges: SaleCataloguesAdd_saleCataloguesAdd_sale_variants_edges[];
pageInfo: SaleCataloguesAdd_saleCataloguesAdd_sale_variants_pageInfo;
totalCount: number | null;
}
export interface SaleCataloguesAdd_saleCataloguesAdd_sale_products_edges_node_productType {
__typename: "ProductType";
id: string;
@ -174,6 +238,7 @@ export interface SaleCataloguesAdd_saleCataloguesAdd_sale {
startDate: any;
endDate: any | null;
channelListings: SaleCataloguesAdd_saleCataloguesAdd_sale_channelListings[] | null;
variants: SaleCataloguesAdd_saleCataloguesAdd_sale_variants | null;
products: SaleCataloguesAdd_saleCataloguesAdd_sale_products | null;
categories: SaleCataloguesAdd_saleCataloguesAdd_sale_categories | null;
collections: SaleCataloguesAdd_saleCataloguesAdd_sale_collections | null;

View file

@ -43,6 +43,70 @@ export interface SaleCataloguesRemove_saleCataloguesRemove_sale_channelListings
currency: string;
}
export interface SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node_product_thumbnail {
__typename: "Image";
url: string;
}
export interface SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node_product_productType {
__typename: "ProductType";
id: string;
name: string;
}
export interface SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node_product_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
currencyCode: string;
}
export interface SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node_product_channelListings {
__typename: "ProductChannelListing";
isPublished: boolean;
publicationDate: any | null;
isAvailableForPurchase: boolean | null;
availableForPurchase: any | null;
visibleInListings: boolean;
channel: SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node_product_channelListings_channel;
}
export interface SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node_product {
__typename: "Product";
id: string;
name: string;
thumbnail: SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node_product_thumbnail | null;
productType: SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node_product_productType;
channelListings: SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node_product_channelListings[] | null;
}
export interface SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node {
__typename: "ProductVariant";
id: string;
name: string;
product: SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node_product;
}
export interface SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges {
__typename: "ProductVariantCountableEdge";
node: SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges_node;
}
export interface SaleCataloguesRemove_saleCataloguesRemove_sale_variants_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SaleCataloguesRemove_saleCataloguesRemove_sale_variants {
__typename: "ProductVariantCountableConnection";
edges: SaleCataloguesRemove_saleCataloguesRemove_sale_variants_edges[];
pageInfo: SaleCataloguesRemove_saleCataloguesRemove_sale_variants_pageInfo;
totalCount: number | null;
}
export interface SaleCataloguesRemove_saleCataloguesRemove_sale_products_edges_node_productType {
__typename: "ProductType";
id: string;
@ -174,6 +238,7 @@ export interface SaleCataloguesRemove_saleCataloguesRemove_sale {
startDate: any;
endDate: any | null;
channelListings: SaleCataloguesRemove_saleCataloguesRemove_sale_channelListings[] | null;
variants: SaleCataloguesRemove_saleCataloguesRemove_sale_variants | null;
products: SaleCataloguesRemove_saleCataloguesRemove_sale_products | null;
categories: SaleCataloguesRemove_saleCataloguesRemove_sale_categories | null;
collections: SaleCataloguesRemove_saleCataloguesRemove_sale_collections | null;

View file

@ -36,6 +36,70 @@ export interface SaleDetails_sale_channelListings {
currency: string;
}
export interface SaleDetails_sale_variants_edges_node_product_thumbnail {
__typename: "Image";
url: string;
}
export interface SaleDetails_sale_variants_edges_node_product_productType {
__typename: "ProductType";
id: string;
name: string;
}
export interface SaleDetails_sale_variants_edges_node_product_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
currencyCode: string;
}
export interface SaleDetails_sale_variants_edges_node_product_channelListings {
__typename: "ProductChannelListing";
isPublished: boolean;
publicationDate: any | null;
isAvailableForPurchase: boolean | null;
availableForPurchase: any | null;
visibleInListings: boolean;
channel: SaleDetails_sale_variants_edges_node_product_channelListings_channel;
}
export interface SaleDetails_sale_variants_edges_node_product {
__typename: "Product";
id: string;
name: string;
thumbnail: SaleDetails_sale_variants_edges_node_product_thumbnail | null;
productType: SaleDetails_sale_variants_edges_node_product_productType;
channelListings: SaleDetails_sale_variants_edges_node_product_channelListings[] | null;
}
export interface SaleDetails_sale_variants_edges_node {
__typename: "ProductVariant";
id: string;
name: string;
product: SaleDetails_sale_variants_edges_node_product;
}
export interface SaleDetails_sale_variants_edges {
__typename: "ProductVariantCountableEdge";
node: SaleDetails_sale_variants_edges_node;
}
export interface SaleDetails_sale_variants_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SaleDetails_sale_variants {
__typename: "ProductVariantCountableConnection";
edges: SaleDetails_sale_variants_edges[];
pageInfo: SaleDetails_sale_variants_pageInfo;
totalCount: number | null;
}
export interface SaleDetails_sale_products_edges_node_productType {
__typename: "ProductType";
id: string;
@ -167,6 +231,7 @@ export interface SaleDetails_sale {
startDate: any;
endDate: any | null;
channelListings: SaleDetails_sale_channelListings[] | null;
variants: SaleDetails_sale_variants | null;
products: SaleDetails_sale_products | null;
categories: SaleDetails_sale_categories | null;
collections: SaleDetails_sale_collections | null;

View file

@ -53,9 +53,11 @@ export type SaleUrlDialog =
| "assign-category"
| "assign-collection"
| "assign-product"
| "assign-variant"
| "unassign-category"
| "unassign-collection"
| "unassign-product"
| "unassign-variant"
| "remove"
| ChannelsAction;
export type SaleUrlQueryParams = Pagination &

View file

@ -11,6 +11,7 @@ import useAppChannel from "@saleor/components/AppLayout/AppChannelContext";
import AssignCategoriesDialog from "@saleor/components/AssignCategoryDialog";
import AssignCollectionDialog from "@saleor/components/AssignCollectionDialog";
import AssignProductDialog from "@saleor/components/AssignProductDialog";
import AssignVariantDialog from "@saleor/components/AssignVariantDialog";
import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA, PAGINATE_BY } from "@saleor/config";
@ -45,7 +46,7 @@ import usePaginator, {
} from "@saleor/hooks/usePaginator";
import { commonMessages, sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc";
import { productUrl } from "@saleor/products/urls";
import { productUrl, productVariantEditPath } from "@saleor/products/urls";
import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import useProductSearch from "@saleor/searches/useProductSearch";
@ -60,6 +61,7 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { createUpdateHandler } from "./handlers";
import { messages } from "./messages";
interface SaleDetailsProps {
id: string;
@ -155,9 +157,7 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
if (data.saleDelete.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Removed sale"
})
text: intl.formatMessage(messages.saleDetailsSaleDeleteDialog)
});
navigate(saleListUrl(), true);
}
@ -197,9 +197,9 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
onChange={channelsToggle}
onClose={handleChannelsModalClose}
open={isChannelsModalOpen}
title={intl.formatMessage({
defaultMessage: "Manage Channel Availability"
})}
title={intl.formatMessage(
messages.saleDetailsChannelAvailabilityDialogHeader
)}
selected={channelListElements.length}
confirmButtonState="default"
onConfirm={handleChannelsConfirm}
@ -219,7 +219,9 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
? maybe(() => data.sale.categories.pageInfo)
: params.activeTab === SaleDetailsPageTab.collections
? maybe(() => data.sale.collections.pageInfo)
: maybe(() => data.sale.products.pageInfo);
: params.activeTab === SaleDetailsPageTab.products
? maybe(() => data.sale.products.pageInfo)
: maybe(() => data.sale.variants.pageInfo);
const handleCategoriesUnassign = (ids: string[]) =>
saleCataloguesRemove({
@ -254,6 +256,17 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
}
});
const handleVariantsUnassign = (ids: string[]) =>
saleCataloguesRemove({
variables: {
...paginationState,
id,
input: {
variants: ids
}
}
});
const {
loadNextPage,
loadPreviousPage,
@ -324,6 +337,14 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
}
onProductClick={id => () =>
navigate(productUrl(id))}
onVariantAssign={() => openModal("assign-variant")}
onVariantUnassign={variantId =>
handleVariantsUnassign([variantId])
}
onVariantClick={(productId, variantId) => () =>
navigate(
productVariantEditPath(productId, variantId)
)}
activeTab={params.activeTab}
onBack={() => navigate(saleListUrl())}
onTabClick={changeTab}
@ -340,9 +361,7 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
}
>
<FormattedMessage
defaultMessage="Unassign"
description="unassign category from sale, button"
id="saleDetailsUnassignCategory"
{...messages.saleDetailsUnassignCategory}
/>
</Button>
}
@ -356,9 +375,7 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
}
>
<FormattedMessage
defaultMessage="Unassign"
description="unassign collection from sale, button"
id="saleDetailsUnassignCollection"
{...messages.saleDetailsUnassignCollection}
/>
</Button>
}
@ -372,9 +389,21 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
}
>
<FormattedMessage
defaultMessage="Unassign"
description="unassign product from sale, button"
id="saleDetailsUnassignProduct"
{...messages.saleDetailsUnassignProduct}
/>
</Button>
}
variantListToolbar={
<Button
color="primary"
onClick={() =>
openModal("unassign-variant", {
ids: listElements
})
}
>
<FormattedMessage
{...messages.saleDetailsUnassignVariant}
/>
</Button>
}
@ -383,6 +412,34 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
toggle={toggle}
toggleAll={toggleAll}
/>
<AssignVariantDialog
confirmButtonState={saleCataloguesAddOpts.status}
hasMore={
searchProductsOpts.data?.search.pageInfo
.hasNextPage
}
open={params.action === "assign-variant"}
onFetch={searchProducts}
onFetchMore={loadMoreProducts}
loading={searchProductsOpts.loading}
onClose={closeModal}
onSubmit={variants =>
saleCataloguesAdd({
variables: {
...paginationState,
id,
input: {
variants: variants.map(
variant => variant.id
)
}
}
})
}
products={mapEdgesToItems(
searchProductsOpts?.data?.search
)?.filter(suggestedProduct => suggestedProduct.id)}
/>
<AssignProductDialog
confirmButtonState={saleCataloguesAddOpts.status}
hasMore={
@ -400,9 +457,7 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
...paginationState,
id,
input: {
products: products.map(
product => product.id
)
products
}
}
})
@ -472,10 +527,9 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
params.action === "unassign-category" &&
canOpenBulkActionDialog
}
title={intl.formatMessage({
defaultMessage: "Unassign Categories From Sale",
description: "dialog header"
})}
title={intl.formatMessage(
messages.saleDetailsUnassignCategoryDialogHeader
)}
confirmButtonState={saleCataloguesRemoveOpts.status}
onClose={closeModal}
onConfirm={() =>
@ -485,8 +539,7 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
{canOpenBulkActionDialog && (
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to unassign this category?} other{Are you sure you want to unassign {displayQuantity} categories?}}"
description="dialog content"
{...messages.saleDetailsUnassignCategoryDialog}
values={{
counter: params.ids.length,
displayQuantity: (
@ -502,10 +555,9 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
params.action === "unassign-collection" &&
canOpenBulkActionDialog
}
title={intl.formatMessage({
defaultMessage: "Unassign Collections From Sale",
description: "dialog header"
})}
title={intl.formatMessage(
messages.saleDetailsUnassignCollectionDialogHeader
)}
confirmButtonState={saleCataloguesRemoveOpts.status}
onClose={closeModal}
onConfirm={() =>
@ -515,8 +567,7 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
{canOpenBulkActionDialog && (
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to unassign this collection?} other{Are you sure you want to unassign {displayQuantity} collections?}}"
description="dialog content"
{...messages.saleDetailsUnassignCollectionDialog}
values={{
counter: params.ids.length,
displayQuantity: (
@ -532,10 +583,9 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
params.action === "unassign-product" &&
canOpenBulkActionDialog
}
title={intl.formatMessage({
defaultMessage: "Unassign Products From Sale",
description: "dialog header"
})}
title={intl.formatMessage(
messages.saleDetailsUnassignProductDialogHeader
)}
confirmButtonState={saleCataloguesRemoveOpts.status}
onClose={closeModal}
onConfirm={() => handleProductsUnassign(params.ids)}
@ -543,8 +593,33 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
{canOpenBulkActionDialog && (
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to unassign this product?} other{Are you sure you want to unassign {displayQuantity} products?}}"
description="dialog content"
{...messages.saleDetailsUnassignCategoryDialog}
values={{
counter: params.ids.length,
displayQuantity: (
<strong>{params.ids.length}</strong>
)
}}
/>
</DialogContentText>
)}
</ActionDialog>
<ActionDialog
open={
params.action === "unassign-variant" &&
canOpenBulkActionDialog
}
title={intl.formatMessage(
messages.saleDetailsUnassignVariantDialogHeader
)}
confirmButtonState={saleCataloguesRemoveOpts.status}
onClose={closeModal}
onConfirm={() => handleVariantsUnassign(params.ids)}
>
{canOpenBulkActionDialog && (
<DialogContentText>
<FormattedMessage
{...messages.saleDetailsUnassignVariantDialog}
values={{
counter: params.ids.length,
displayQuantity: (
@ -557,10 +632,9 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
</ActionDialog>
<ActionDialog
open={params.action === "remove"}
title={intl.formatMessage({
defaultMessage: "Delete Sale",
description: "dialog header"
})}
title={intl.formatMessage(
messages.saleDetailsSaleDeleteDialogHeader
)}
confirmButtonState={saleDeleteOpts.status}
onClose={closeModal}
variant="delete"
@ -572,8 +646,7 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {saleName}?"
description="dialog content"
{...messages.saleDetailsUnassignDialogDelete}
values={{
saleName: (
<strong>

View file

@ -0,0 +1,76 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
saleDetailsUnassignCategory: {
defaultMessage: "Unassign",
description: "unassign category from sale, button"
},
saleDetailsUnassignCollection: {
defaultMessage: "Unassign",
description: "unassign collection from sale, button"
},
saleDetailsUnassignProduct: {
defaultMessage: "Unassign",
description: "unassign product from sale, button"
},
saleDetailsUnassignVariant: {
defaultMessage: "Unassign",
description: "unassign variant from sale, button"
},
saleDetailsUnassignCategoryDialog: {
defaultMessage:
"{counter,plural,one{Are you sure you want to unassign this category?} other{Are you sure you want to unassign {displayQuantity} categories?}}",
description: "dialog content"
},
saleDetailsUnassignCollectionDialog: {
defaultMessage:
"{counter,plural,one{Are you sure you want to unassign this collection?} other{Are you sure you want to unassign {displayQuantity} collections?}}",
description: "dialog content"
},
saleDetailsUnassignProductDialog: {
defaultMessage:
"{counter,plural,one{Are you sure you want to unassign this product?} other{Are you sure you want to unassign {displayQuantity} products?}}",
description: "dialog content"
},
saleDetailsUnassignVariantDialog: {
defaultMessage:
"{counter,plural,one{Are you sure you want to unassign this variant?} other{Are you sure you want to unassign {displayQuantity} variants?}}",
description: "dialog content"
},
saleDetailsUnassignDialogDelete: {
defaultMessage: "Are you sure you want to delete {saleName}?",
description: "dialog content"
},
saleDetailsSaleDelate: {
defaultMessage: "Removed sale",
description: "sale Details delete button"
},
saleDetailsUnassignCategoryDialogHeader: {
defaultMessage: "Unassign Categories From Sale",
description: "dialog header"
},
saleDetailsUnassignCollectionDialogHeader: {
defaultMessage: "Unassign Collection From Sale",
description: "dialog header"
},
saleDetailsUnassignProductDialogHeader: {
defaultMessage: "Unassign Product From Sale",
description: "dialog header"
},
saleDetailsUnassignVariantDialogHeader: {
defaultMessage: "Unassign Variant From Sale",
description: "dialog header"
},
saleDetailsSaleDeleteDialog: {
defaultMessage: "Removed sale",
description: "dialog content"
},
saleDetailsSaleDeleteDialogHeader: {
defaultMessage: "Delete Sale",
description: "dialog header"
},
saleDetailsChannelAvailabilityDialogHeader: {
defaultMessage: "Manage Channel Availability",
description: "channel availability dialog header"
}
});

View file

@ -533,9 +533,7 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
...paginationState,
id,
input: {
products: products.map(
product => product.id
)
products
}
}
})

View file

@ -32,6 +32,32 @@ export const saleDetailsFragment = gql`
${saleFragment}
fragment SaleDetailsFragment on Sale {
...SaleFragment
variants(after: $after, before: $before, first: $first, last: $last) {
edges {
node {
id
name
product {
id
name
thumbnail {
url
}
productType {
id
name
}
channelListings {
...ChannelListingProductWithoutPricingFragment
}
}
}
}
pageInfo {
...PageInfoFragment
}
totalCount
}
products(after: $after, before: $before, first: $first, last: $last) {
edges {
node {

View file

@ -36,6 +36,70 @@ export interface SaleDetailsFragment_channelListings {
currency: string;
}
export interface SaleDetailsFragment_variants_edges_node_product_thumbnail {
__typename: "Image";
url: string;
}
export interface SaleDetailsFragment_variants_edges_node_product_productType {
__typename: "ProductType";
id: string;
name: string;
}
export interface SaleDetailsFragment_variants_edges_node_product_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
currencyCode: string;
}
export interface SaleDetailsFragment_variants_edges_node_product_channelListings {
__typename: "ProductChannelListing";
isPublished: boolean;
publicationDate: any | null;
isAvailableForPurchase: boolean | null;
availableForPurchase: any | null;
visibleInListings: boolean;
channel: SaleDetailsFragment_variants_edges_node_product_channelListings_channel;
}
export interface SaleDetailsFragment_variants_edges_node_product {
__typename: "Product";
id: string;
name: string;
thumbnail: SaleDetailsFragment_variants_edges_node_product_thumbnail | null;
productType: SaleDetailsFragment_variants_edges_node_product_productType;
channelListings: SaleDetailsFragment_variants_edges_node_product_channelListings[] | null;
}
export interface SaleDetailsFragment_variants_edges_node {
__typename: "ProductVariant";
id: string;
name: string;
product: SaleDetailsFragment_variants_edges_node_product;
}
export interface SaleDetailsFragment_variants_edges {
__typename: "ProductVariantCountableEdge";
node: SaleDetailsFragment_variants_edges_node;
}
export interface SaleDetailsFragment_variants_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SaleDetailsFragment_variants {
__typename: "ProductVariantCountableConnection";
edges: SaleDetailsFragment_variants_edges[];
pageInfo: SaleDetailsFragment_variants_pageInfo;
totalCount: number | null;
}
export interface SaleDetailsFragment_products_edges_node_productType {
__typename: "ProductType";
id: string;
@ -167,6 +231,7 @@ export interface SaleDetailsFragment {
startDate: any;
endDate: any | null;
channelListings: SaleDetailsFragment_channelListings[] | null;
variants: SaleDetailsFragment_variants | null;
products: SaleDetailsFragment_products | null;
categories: SaleDetailsFragment_categories | null;
collections: SaleDetailsFragment_collections | null;

View file

@ -12,11 +12,40 @@ export interface SearchProducts_search_edges_node_thumbnail {
url: string;
}
export interface SearchProducts_search_edges_node_variants_channelListings_channel {
__typename: "Channel";
id: string;
isActive: boolean;
name: string;
currencyCode: string;
}
export interface SearchProducts_search_edges_node_variants_channelListings_price {
__typename: "Money";
amount: number;
currency: string;
}
export interface SearchProducts_search_edges_node_variants_channelListings {
__typename: "ProductVariantChannelListing";
channel: SearchProducts_search_edges_node_variants_channelListings_channel;
price: SearchProducts_search_edges_node_variants_channelListings_price | null;
}
export interface SearchProducts_search_edges_node_variants {
__typename: "ProductVariant";
id: string;
name: string;
sku: string;
channelListings: SearchProducts_search_edges_node_variants_channelListings[] | null;
}
export interface SearchProducts_search_edges_node {
__typename: "Product";
id: string;
name: string;
thumbnail: SearchProducts_search_edges_node_thumbnail | null;
variants: (SearchProducts_search_edges_node_variants | null)[] | null;
}
export interface SearchProducts_search_edges {

View file

@ -0,0 +1,55 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: SearchVariants
// ====================================================
export interface SearchVariants_search_edges_node_product_thumbnail {
__typename: "Image";
url: string;
}
export interface SearchVariants_search_edges_node_product {
__typename: "Product";
name: string;
thumbnail: SearchVariants_search_edges_node_product_thumbnail | null;
}
export interface SearchVariants_search_edges_node {
__typename: "ProductVariant";
id: string;
name: string;
product: SearchVariants_search_edges_node_product;
}
export interface SearchVariants_search_edges {
__typename: "ProductVariantCountableEdge";
node: SearchVariants_search_edges_node;
}
export interface SearchVariants_search_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface SearchVariants_search {
__typename: "ProductVariantCountableConnection";
edges: SearchVariants_search_edges[];
pageInfo: SearchVariants_search_pageInfo;
}
export interface SearchVariants {
search: SearchVariants_search | null;
}
export interface SearchVariantsVariables {
after?: string | null;
first: number;
query: string;
}

View file

@ -18,6 +18,23 @@ export const searchProducts = gql`
thumbnail {
url
}
variants {
id
name
sku
channelListings {
channel {
id
isActive
name
currencyCode
}
price {
amount
currency
}
}
}
}
}
pageInfo {

View file

@ -1873,15 +1873,46 @@ export const products: SearchProducts_search_edges_node[] = [
thumbnail: {
__typename: "Image",
url: ""
}
},
{
__typename: "Product",
id: "2",
name: "Banana Juice",
thumbnail: {
__typename: "Image",
url: ""
}
},
variants: [
{
__typename: "ProductVariant",
id: "UHJvZHVjdFZhcmlhbnQ6MjAz",
name: "1l",
sku: "43226647",
channelListings: [
{
__typename: "ProductVariantChannelListing",
channel: {
__typename: "Channel",
id: "Q2hhbm5lbDox",
isActive: true,
name: "Channel-USD",
currencyCode: "USD"
},
price: {
__typename: "Money",
amount: 5,
currency: "USD"
}
},
{
__typename: "ProductVariantChannelListing",
channel: {
__typename: "Channel",
id: "Q2hhbm5lbDoy",
isActive: true,
name: "Channel-PLN",
currencyCode: "PLN"
},
price: {
__typename: "Money",
amount: 20,
currency: "PLN"
}
}
]
}
]
}
];

View file

@ -88061,6 +88061,12 @@ exports[`Storyshots Views / Discounts / Sale details collections 1`] = `
>
Products (4)
</span>
<span
class="MuiTypography-root-id Tab-root-id MuiTypography-body1-id"
data-test-id="variants-tab"
>
Variants (3)
</span>
</div>
<div
class="CardSpacer-spacer-id"
@ -88155,10 +88161,10 @@ exports[`Storyshots Views / Discounts / Sale details collections 1`] = `
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCollections-colName-id"
scope="col"
>
Collection name
Collection Name
</th>
<th
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCollections-textRight-id"
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCollections-colProducts-id"
scope="col"
>
Products
@ -89484,6 +89490,12 @@ exports[`Storyshots Views / Discounts / Sale details default 1`] = `
>
Products (4)
</span>
<span
class="MuiTypography-root-id Tab-root-id MuiTypography-body1-id"
data-test-id="variants-tab"
>
Variants (3)
</span>
</div>
<div
class="CardSpacer-spacer-id"
@ -89578,7 +89590,7 @@ exports[`Storyshots Views / Discounts / Sale details default 1`] = `
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colName-id"
scope="col"
>
Category name
Category Name
</th>
<th
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colProducts-id"
@ -90912,6 +90924,12 @@ exports[`Storyshots Views / Discounts / Sale details form errors 1`] = `
>
Products (4)
</span>
<span
class="MuiTypography-root-id Tab-root-id MuiTypography-body1-id"
data-test-id="variants-tab"
>
Variants (3)
</span>
</div>
<div
class="CardSpacer-spacer-id"
@ -91006,7 +91024,7 @@ exports[`Storyshots Views / Discounts / Sale details form errors 1`] = `
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colName-id"
scope="col"
>
Category name
Category Name
</th>
<th
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colProducts-id"
@ -92363,6 +92381,12 @@ exports[`Storyshots Views / Discounts / Sale details loading 1`] = `
>
Products (…)
</span>
<span
class="MuiTypography-root-id Tab-root-id MuiTypography-body1-id"
data-test-id="variants-tab"
>
Variants (…)
</span>
</div>
<div
class="CardSpacer-spacer-id"
@ -92459,7 +92483,7 @@ exports[`Storyshots Views / Discounts / Sale details loading 1`] = `
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colName-id"
scope="col"
>
Category name
Category Name
</th>
<th
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colProducts-id"
@ -93778,6 +93802,12 @@ exports[`Storyshots Views / Discounts / Sale details products 1`] = `
>
Products (4)
</span>
<span
class="MuiTypography-root-id Tab-root-id MuiTypography-body1-id"
data-test-id="variants-tab"
>
Variants (3)
</span>
</div>
<div
class="CardSpacer-spacer-id"

View file

@ -1,9 +1,8 @@
import placeholderImage from "@assets/images/placeholder60x60.png";
import AssignProductDialog, {
AssignProductDialogProps
} from "@saleor/components/AssignProductDialog";
import { fetchMoreProps } from "@saleor/fixtures";
import { products } from "@saleor/products/fixtures";
import { products } from "@saleor/shipping/fixtures";
import { storiesOf } from "@storybook/react";
import React from "react";
@ -17,7 +16,7 @@ const props: AssignProductDialogProps = {
onFetch: () => undefined,
onSubmit: () => undefined,
open: true,
products: products(placeholderImage)
products
};
storiesOf("Generics / Assign product", module)

View file

@ -36,6 +36,9 @@ const props: SaleDetailsPageProps = {
onProductAssign: () => undefined,
onProductClick: () => undefined,
onProductUnassign: () => undefined,
onVariantAssign: () => undefined,
onVariantClick: () => undefined,
onVariantUnassign: () => undefined,
onRemove: () => undefined,
onSubmit: () => undefined,
onTabClick: () => undefined,
@ -45,6 +48,7 @@ const props: SaleDetailsPageProps = {
hasPreviousPage: false
},
productListToolbar: null,
variantListToolbar: null,
sale,
saveButtonBarState: "default",
selectedChannelId: "123",

View file

@ -2018,6 +2018,7 @@ export interface CatalogueInput {
products?: (string | null)[] | null;
categories?: (string | null)[] | null;
collections?: (string | null)[] | null;
variants?: (string | null)[] | null;
}
export interface CategoryFilterInput {
@ -2674,6 +2675,7 @@ export interface SaleInput {
type?: DiscountValueTypeEnum | null;
value?: any | null;
products?: (string | null)[] | null;
variants?: (string | null)[] | null;
categories?: (string | null)[] | null;
collections?: (string | null)[] | null;
startDate?: any | null;