diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 5ac076a6f..3a0e3d17b 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -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" diff --git a/schema.graphql b/schema.graphql index 474c04136..2776e94f4 100644 --- a/schema.graphql +++ b/schema.graphql @@ -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 -} \ No newline at end of file +} diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index d031077ec..199bf888f 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -358,7 +358,7 @@ export const CollectionDetails: React.FC = ({ variables: { ...paginationState, collectionId: id, - productIds: products.map(product => product.id) + productIds: products } }) } diff --git a/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx b/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx index f0077a198..994cc02b4 100644 --- a/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx +++ b/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx @@ -5,6 +5,7 @@ import { useIntl } from "react-intl"; import AssignContainerDialog, { AssignContainerDialogProps } from "../AssignContainerDialog"; +import { messages } from "./messages"; interface AssignCategoryDialogProps extends Omit { @@ -21,17 +22,12 @@ const AssignCategoryDialog: React.FC = ({ ); diff --git a/src/components/AssignCategoryDialog/messages.ts b/src/components/AssignCategoryDialog/messages.ts new file mode 100644 index 000000000..9c7d7993e --- /dev/null +++ b/src/components/AssignCategoryDialog/messages.ts @@ -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" + } +}); diff --git a/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx b/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx index 0ee4d7146..503acefcb 100644 --- a/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx +++ b/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx @@ -5,6 +5,7 @@ import { useIntl } from "react-intl"; import AssignContainerDialog, { AssignContainerDialogProps } from "../AssignContainerDialog"; +import { messages } from "./messages"; interface AssignCollectionDialogProps extends Omit { @@ -21,17 +22,12 @@ const AssignCollectionDialog: React.FC = ({ ); diff --git a/src/components/AssignCollectionDialog/messages.ts b/src/components/AssignCollectionDialog/messages.ts new file mode 100644 index 000000000..a6ca46533 --- /dev/null +++ b/src/components/AssignCollectionDialog/messages.ts @@ -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" + } +}); diff --git a/src/components/AssignContainerDialog/AssignContainerDialog.tsx b/src/components/AssignContainerDialog/AssignContainerDialog.tsx index e7a5a79b1..26584b2b9 100644 --- a/src/components/AssignContainerDialog/AssignContainerDialog.tsx +++ b/src/components/AssignContainerDialog/AssignContainerDialog.tsx @@ -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 = props => { type="submit" onClick={handleSubmit} > - + diff --git a/src/components/AssignContainerDialog/messages.ts b/src/components/AssignContainerDialog/messages.ts new file mode 100644 index 000000000..8b33f12f0 --- /dev/null +++ b/src/components/AssignContainerDialog/messages.ts @@ -0,0 +1,8 @@ +import { defineMessages } from "react-intl"; + +export const messages = defineMessages({ + assignContainerDialogButton: { + defaultMessage: "Assign", + description: "button" + } +}); diff --git a/src/components/AssignContainerDialog/styles.ts b/src/components/AssignContainerDialog/styles.ts new file mode 100644 index 000000000..a74516512 --- /dev/null +++ b/src/components/AssignContainerDialog/styles.ts @@ -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" } +); diff --git a/src/components/AssignProductDialog/AssignProductDialog.tsx b/src/components/AssignProductDialog/AssignProductDialog.tsx index 9d9fdc61f..dcc640211 100644 --- a/src/components/AssignProductDialog/AssignProductDialog.tsx +++ b/src/components/AssignProductDialog/AssignProductDialog.tsx @@ -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 = props => { const intl = useIntl(); const [query, onQueryChange] = useSearchQuery(onFetch); - const [selectedProducts, setSelectedProducts] = React.useState< - SearchProducts_search_edges_node[] - >([]); + const [selectedProducts, setSelectedProducts] = React.useState([]); const handleSubmit = () => onSubmit(selectedProducts); @@ -113,23 +89,15 @@ const AssignProductDialog: React.FC = props => { maxWidth="sm" > - + = 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 = props => { checked={isSelected} onChange={() => handleProductAssign( - product, + product.id, isSelected, selectedProducts, setSelectedProducts @@ -208,10 +176,7 @@ const AssignProductDialog: React.FC = props => { type="submit" onClick={handleSubmit} > - + diff --git a/src/components/AssignProductDialog/messages.ts b/src/components/AssignProductDialog/messages.ts new file mode 100644 index 000000000..88b131e91 --- /dev/null +++ b/src/components/AssignProductDialog/messages.ts @@ -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..." + } +}); diff --git a/src/components/AssignProductDialog/styles.ts b/src/components/AssignProductDialog/styles.ts new file mode 100644 index 000000000..3493e18fa --- /dev/null +++ b/src/components/AssignProductDialog/styles.ts @@ -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" } +); diff --git a/src/components/AssignVariantDialog/AssignVariantDialog.tsx b/src/components/AssignVariantDialog/AssignVariantDialog.tsx new file mode 100644 index 000000000..cb22bd5ec --- /dev/null +++ b/src/components/AssignVariantDialog/AssignVariantDialog.tsx @@ -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 = 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 ( + + + + + + + }} + /> + + + + + + } + scrollableTarget={scrollableTargetId} + > + + + {renderCollection( + products, + (product, productIndex) => ( + + + + + handleProductAssign( + product, + productIndex, + productsWithAllVariantsSelected, + variants, + setVariants + ) + } + /> + + product.thumbnail.url)} + /> + + {maybe(() => product.name)} + + + {maybe(() => product.variants, []).map( + (variant, variantIndex) => ( + + + + + handleVariantAssign( + variant, + variantIndex, + productIndex, + variants, + selectedVariantsToProductsMap, + setVariants + ) + } + /> + + +
{variant.name}
+
+ +
+
+ + {variant?.channelListings[0]?.price && ( + + )} + +
+ ) + )} +
+ ), + () => ( + + + + + + ) + )} +
+
+
+
+ + + + + + +
+ ); +}; +AssignVariantDialog.displayName = "AssignVariantDialog"; +export default AssignVariantDialog; diff --git a/src/components/AssignVariantDialog/index.ts b/src/components/AssignVariantDialog/index.ts new file mode 100644 index 000000000..ed0b1cde3 --- /dev/null +++ b/src/components/AssignVariantDialog/index.ts @@ -0,0 +1,2 @@ +export { default } from "./AssignVariantDialog"; +export * from "./AssignVariantDialog"; diff --git a/src/components/AssignVariantDialog/messages.ts b/src/components/AssignVariantDialog/messages.ts new file mode 100644 index 000000000..b63d5a99d --- /dev/null +++ b/src/components/AssignVariantDialog/messages.ts @@ -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" + } +}); diff --git a/src/components/AssignVariantDialog/styles.ts b/src/components/AssignVariantDialog/styles.ts new file mode 100644 index 000000000..962dae597 --- /dev/null +++ b/src/components/AssignVariantDialog/styles.ts @@ -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" } +); diff --git a/src/discounts/components/DiscountCategories/DiscountCategories.tsx b/src/discounts/components/DiscountCategories/DiscountCategories.tsx index 08381506d..65ed891b4 100644 --- a/src/discounts/components/DiscountCategories/DiscountCategories.tsx +++ b/src/discounts/components/DiscountCategories/DiscountCategories.tsx @@ -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 = props => { @@ -77,16 +55,10 @@ const DiscountCategories: React.FC = props => { return ( - + } /> @@ -107,12 +79,13 @@ const DiscountCategories: React.FC = props => { > <> - + @@ -179,7 +152,7 @@ const DiscountCategories: React.FC = props => { () => ( - + ) diff --git a/src/discounts/components/DiscountCategories/messages.ts b/src/discounts/components/DiscountCategories/messages.ts new file mode 100644 index 000000000..b24db817c --- /dev/null +++ b/src/discounts/components/DiscountCategories/messages.ts @@ -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" + } +}); diff --git a/src/discounts/components/DiscountCategories/styles.ts b/src/discounts/components/DiscountCategories/styles.ts new file mode 100644 index 000000000..80c0940ad --- /dev/null +++ b/src/discounts/components/DiscountCategories/styles.ts @@ -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" } +); diff --git a/src/discounts/components/DiscountCollections/DiscountCollections.tsx b/src/discounts/components/DiscountCollections/DiscountCollections.tsx index a819273f8..36e945409 100644 --- a/src/discounts/components/DiscountCollections/DiscountCollections.tsx +++ b/src/discounts/components/DiscountCollections/DiscountCollections.tsx @@ -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 = props => { @@ -79,16 +56,10 @@ const DiscountCollections: React.FC = props => { return ( - + } /> @@ -108,12 +79,13 @@ const DiscountCollections: React.FC = props => { toolbar={toolbar} > - - - + + + @@ -181,7 +153,7 @@ const DiscountCollections: React.FC = props => { () => ( - + ) diff --git a/src/discounts/components/DiscountCollections/messages.ts b/src/discounts/components/DiscountCollections/messages.ts new file mode 100644 index 000000000..be66a8e5f --- /dev/null +++ b/src/discounts/components/DiscountCollections/messages.ts @@ -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" + } +}); diff --git a/src/discounts/components/DiscountCollections/styles.ts b/src/discounts/components/DiscountCollections/styles.ts new file mode 100644 index 000000000..90cbe9be6 --- /dev/null +++ b/src/discounts/components/DiscountCollections/styles.ts @@ -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" } +); diff --git a/src/discounts/components/DiscountProducts/DiscountProducts.tsx b/src/discounts/components/DiscountProducts/DiscountProducts.tsx index 53bb5f444..ebb500211 100644 --- a/src/discounts/components/DiscountProducts/DiscountProducts.tsx +++ b/src/discounts/components/DiscountProducts/DiscountProducts.tsx @@ -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 = props => { const { channelsCount, - discount: sale, + products, disabled, pageInfo, onRowClick, @@ -91,20 +60,14 @@ const DiscountProducts: React.FC = props => { return ( - + } /> @@ -120,22 +83,23 @@ const DiscountProducts: React.FC = props => { colSpan={numberOfColumns} selected={selected} disabled={disabled} - items={mapEdgesToItems(sale?.products)} + items={products} toggleAll={toggleAll} toolbar={toolbar} > - - + 0 && classes.colNameLabel}> + - + @@ -155,7 +119,7 @@ const DiscountProducts: React.FC = props => { {renderCollection( - mapEdgesToItems(sale?.products), + products, product => { const isSelected = product ? isChecked(product.id) : false; @@ -216,7 +180,7 @@ const DiscountProducts: React.FC = props => { () => ( - + ) diff --git a/src/discounts/components/DiscountProducts/messages.ts b/src/discounts/components/DiscountProducts/messages.ts new file mode 100644 index 000000000..351f13b87 --- /dev/null +++ b/src/discounts/components/DiscountProducts/messages.ts @@ -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" + } +}); diff --git a/src/discounts/components/DiscountProducts/styles.ts b/src/discounts/components/DiscountProducts/styles.ts new file mode 100644 index 000000000..9bd786e03 --- /dev/null +++ b/src/discounts/components/DiscountProducts/styles.ts @@ -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" } +); diff --git a/src/discounts/components/DiscountVariants/DiscountVariants.tsx b/src/discounts/components/DiscountVariants/DiscountVariants.tsx new file mode 100644 index 000000000..cffed9f7e --- /dev/null +++ b/src/discounts/components/DiscountVariants/DiscountVariants.tsx @@ -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, + 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 = props => { + const { + variants, + disabled, + pageInfo, + onRowClick, + onPreviousPage, + onVariantAssign, + onVariantUnassign, + onNextPage, + isChecked, + selected, + toggle, + toggleAll, + toolbar + } = props; + const classes = useStyles(props); + + const intl = useIntl(); + + return ( + + + + + } + /> + + + + + + + + + + + 0 && classes.colNameLabel}> + + + + + + + + + + + + + + + + + + {renderCollection( + variants, + variant => { + const isSelected = variant ? isChecked(variant.id) : false; + + return ( + + + toggle(variant.id)} + /> + + variant.product.thumbnail.url)} + > + {maybe( + () => variant.product.name, + + )} + + + {maybe(() => variant.name, )} + + + {maybe( + () => variant.product.productType.name, + + )} + + + { + event.stopPropagation(); + onVariantUnassign(variant.id); + }} + > + + + + + ); + }, + () => ( + + + + + + ) + )} + + + + ); +}; +DiscountVariants.displayName = "DiscountVariants"; +export default DiscountVariants; diff --git a/src/discounts/components/DiscountVariants/index.ts b/src/discounts/components/DiscountVariants/index.ts new file mode 100644 index 000000000..383764c3b --- /dev/null +++ b/src/discounts/components/DiscountVariants/index.ts @@ -0,0 +1,2 @@ +export { default } from "./DiscountVariants"; +export * from "./DiscountVariants"; diff --git a/src/discounts/components/DiscountVariants/messages.ts b/src/discounts/components/DiscountVariants/messages.ts new file mode 100644 index 000000000..cf3e85c4f --- /dev/null +++ b/src/discounts/components/DiscountVariants/messages.ts @@ -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" + } +}); diff --git a/src/discounts/components/DiscountVariants/styles.ts b/src/discounts/components/DiscountVariants/styles.ts new file mode 100644 index 000000000..277f66f22 --- /dev/null +++ b/src/discounts/components/DiscountVariants/styles.ts @@ -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" } +); diff --git a/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx b/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx index 7e1c67e15..7b16072d2 100644 --- a/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx +++ b/src/discounts/components/SaleDetailsPage/SaleDetailsPage.tsx @@ -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>, 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 = ({ activeTab, @@ -120,9 +133,13 @@ const SaleDetailsPage: React.FC = ({ onProductAssign, onProductUnassign, onProductClick, + onVariantAssign, + onVariantUnassign, + onVariantClick, categoryListToolbar, collectionListToolbar, productListToolbar, + variantListToolbar, isChecked, selected, selectedChannelId, @@ -239,6 +256,25 @@ const SaleDetailsPage: React.FC = ({ } )} + + {intl.formatMessage( + { + defaultMessage: "Variants ({quantity})", + description: "number of variants", + id: "saleDetailsPageVariantsQuantity" + }, + { + quantity: maybe( + () => sale.variants.totalCount.toString(), + "…" + ) + } + )} + {activeTab === SaleDetailsPageTab.categories ? ( @@ -273,7 +309,7 @@ const SaleDetailsPage: React.FC = ({ toggleAll={toggleAll} toolbar={collectionListToolbar} /> - ) : ( + ) : activeTab === SaleDetailsPageTab.products ? ( = ({ 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 = ({ toggleAll={toggleAll} toolbar={productListToolbar} /> + ) : ( + )} = ({ onProductUnassign={onProductUnassign} onRowClick={onProductClick} pageInfo={pageInfo} - discount={voucher} + products={mapEdgesToItems(voucher.products)} channelsCount={allChannelsCount} isChecked={isChecked} selected={selected} diff --git a/src/discounts/fixtures.ts b/src/discounts/fixtures.ts index 9a3e7a302..527598428 100644 --- a/src/discounts/fixtures.ts +++ b/src/discounts/fixtures.ts @@ -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 }; diff --git a/src/discounts/types/SaleCataloguesAdd.ts b/src/discounts/types/SaleCataloguesAdd.ts index 59a333975..9db94ff0c 100644 --- a/src/discounts/types/SaleCataloguesAdd.ts +++ b/src/discounts/types/SaleCataloguesAdd.ts @@ -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; diff --git a/src/discounts/types/SaleCataloguesRemove.ts b/src/discounts/types/SaleCataloguesRemove.ts index c4fd89ad7..039d8be44 100644 --- a/src/discounts/types/SaleCataloguesRemove.ts +++ b/src/discounts/types/SaleCataloguesRemove.ts @@ -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; diff --git a/src/discounts/types/SaleDetails.ts b/src/discounts/types/SaleDetails.ts index 860aaebdf..381cff5d6 100644 --- a/src/discounts/types/SaleDetails.ts +++ b/src/discounts/types/SaleDetails.ts @@ -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; diff --git a/src/discounts/urls.ts b/src/discounts/urls.ts index d3303ae3f..7e85376b1 100644 --- a/src/discounts/urls.ts +++ b/src/discounts/urls.ts @@ -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 & diff --git a/src/discounts/views/SaleDetails/SaleDetails.tsx b/src/discounts/views/SaleDetails/SaleDetails.tsx index 8ed0bdcb2..f651f80b6 100644 --- a/src/discounts/views/SaleDetails/SaleDetails.tsx +++ b/src/discounts/views/SaleDetails/SaleDetails.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ id, params }) => { } > } @@ -356,9 +375,7 @@ export const SaleDetails: React.FC = ({ id, params }) => { } > } @@ -372,9 +389,21 @@ export const SaleDetails: React.FC = ({ id, params }) => { } > + + } + variantListToolbar={ + } @@ -383,6 +412,34 @@ export const SaleDetails: React.FC = ({ id, params }) => { toggle={toggle} toggleAll={toggleAll} /> + + saleCataloguesAdd({ + variables: { + ...paginationState, + id, + input: { + variants: variants.map( + variant => variant.id + ) + } + } + }) + } + products={mapEdgesToItems( + searchProductsOpts?.data?.search + )?.filter(suggestedProduct => suggestedProduct.id)} + /> = ({ id, params }) => { ...paginationState, id, input: { - products: products.map( - product => product.id - ) + products } } }) @@ -472,10 +527,9 @@ export const SaleDetails: React.FC = ({ 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 = ({ id, params }) => { {canOpenBulkActionDialog && ( = ({ 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 = ({ id, params }) => { {canOpenBulkActionDialog && ( = ({ 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 = ({ id, params }) => { {canOpenBulkActionDialog && ( {params.ids.length} + ) + }} + /> + + )} + + handleVariantsUnassign(params.ids)} + > + {canOpenBulkActionDialog && ( + + = ({ id, params }) => { = ({ id, params }) => { > diff --git a/src/discounts/views/SaleDetails/messages.ts b/src/discounts/views/SaleDetails/messages.ts new file mode 100644 index 000000000..fd3a9063f --- /dev/null +++ b/src/discounts/views/SaleDetails/messages.ts @@ -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" + } +}); diff --git a/src/discounts/views/VoucherDetails/VoucherDetails.tsx b/src/discounts/views/VoucherDetails/VoucherDetails.tsx index f8bda40a8..f1d5ba90e 100644 --- a/src/discounts/views/VoucherDetails/VoucherDetails.tsx +++ b/src/discounts/views/VoucherDetails/VoucherDetails.tsx @@ -533,9 +533,7 @@ export const VoucherDetails: React.FC = ({ ...paginationState, id, input: { - products: products.map( - product => product.id - ) + products } } }) diff --git a/src/fragments/discounts.ts b/src/fragments/discounts.ts index 48972d1fd..aa09a38b1 100644 --- a/src/fragments/discounts.ts +++ b/src/fragments/discounts.ts @@ -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 { diff --git a/src/fragments/types/SaleDetailsFragment.ts b/src/fragments/types/SaleDetailsFragment.ts index 7dc3d36f3..78827b11c 100644 --- a/src/fragments/types/SaleDetailsFragment.ts +++ b/src/fragments/types/SaleDetailsFragment.ts @@ -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; diff --git a/src/searches/types/SearchProducts.ts b/src/searches/types/SearchProducts.ts index 93ab6722f..ad85629c8 100644 --- a/src/searches/types/SearchProducts.ts +++ b/src/searches/types/SearchProducts.ts @@ -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 { diff --git a/src/searches/types/SearchVariants.ts b/src/searches/types/SearchVariants.ts new file mode 100644 index 000000000..5aaa889bb --- /dev/null +++ b/src/searches/types/SearchVariants.ts @@ -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; +} diff --git a/src/searches/useProductSearch.ts b/src/searches/useProductSearch.ts index 1da659a51..185e3014b 100644 --- a/src/searches/useProductSearch.ts +++ b/src/searches/useProductSearch.ts @@ -18,6 +18,23 @@ export const searchProducts = gql` thumbnail { url } + variants { + id + name + sku + channelListings { + channel { + id + isActive + name + currencyCode + } + price { + amount + currency + } + } + } } } pageInfo { diff --git a/src/shipping/fixtures.ts b/src/shipping/fixtures.ts index a09efd131..e91057991 100644 --- a/src/shipping/fixtures.ts +++ b/src/shipping/fixtures.ts @@ -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" + } + } + ] + } + ] } ]; diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index acc8e6924..46f4b4038 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -88061,6 +88061,12 @@ exports[`Storyshots Views / Discounts / Sale details collections 1`] = ` > Products (4) + + Variants (3) +
- Collection name + Collection Name Products @@ -89484,6 +89490,12 @@ exports[`Storyshots Views / Discounts / Sale details default 1`] = ` > Products (4) + + Variants (3) +
- Category name + Category Name Products (4) + + Variants (3) +
- Category name + Category Name Products (…) + + Variants (…) +
- Category name + Category Name Products (4) + + Variants (3) +
undefined, onSubmit: () => undefined, open: true, - products: products(placeholderImage) + products }; storiesOf("Generics / Assign product", module) diff --git a/src/storybook/stories/discounts/SaleDetailsPage.tsx b/src/storybook/stories/discounts/SaleDetailsPage.tsx index 58c2b3d6f..a631e2c65 100644 --- a/src/storybook/stories/discounts/SaleDetailsPage.tsx +++ b/src/storybook/stories/discounts/SaleDetailsPage.tsx @@ -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", diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 94a2ae94f..760f62c67 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -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;