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", "context": "number of products",
"string": "Products ({quantity})" "string": "Products ({quantity})"
}, },
"saleDetailsUnassignCategory": { "saleDetailsPageVariantsQuantity": {
"context": "unassign category from sale, button", "context": "number of variants",
"string": "Unassign" "string": "Variants ({quantity})"
},
"saleDetailsUnassignCollection": {
"context": "unassign collection from sale, button",
"string": "Unassign"
},
"saleDetailsUnassignProduct": {
"context": "unassign product from sale, button",
"string": "Unassign"
}, },
"set availability date": { "set availability date": {
"context": "product availability date label", "context": "product availability date label",
@ -1889,44 +1881,69 @@
"context": "placeholder", "context": "placeholder",
"string": "Search by value name, etc..." "string": "Search by value name, etc..."
}, },
"src_dot_components_dot_AssignCategoryDialog_dot_3125506097": { "src_dot_components_dot_AssignCategoryDialog_dot_assignCategoryDialogHeader": {
"context": "dialog header", "context": "dialog header",
"string": "Assign Category" "string": "Assign Category"
}, },
"src_dot_components_dot_AssignCategoryDialog_dot_3690273268": { "src_dot_components_dot_AssignCategoryDialog_dot_assignCategoryDialogLabel": {
"string": "Search by category name, etc..." "context": "dialog header",
},
"src_dot_components_dot_AssignCategoryDialog_dot_3841025483": {
"string": "Search Category" "string": "Search Category"
}, },
"src_dot_components_dot_AssignCollectionDialog_dot_2605414502": { "src_dot_components_dot_AssignCategoryDialog_dot_assignCategoryDialogPlaceholder": {
"string": "Search by collection name, etc..." "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", "context": "dialog header",
"string": "Assign Collection" "string": "Assign Collection"
}, },
"src_dot_components_dot_AssignCollectionDialog_dot_4057224233": { "src_dot_components_dot_AssignCollectionDialog_dot_assignCollectionDialogLabel": {
"context": "dialog header",
"string": "Search Collection" "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", "context": "button",
"string": "Assign" "string": "Assign"
}, },
"src_dot_components_dot_AssignProductDialog_dot_2100305525": { "src_dot_components_dot_AssignProductDialog_dot_assignProductDialogButton": {
"context": "button", "context": "button",
"string": "Assign products" "string": "Assign"
}, },
"src_dot_components_dot_AssignProductDialog_dot_2336947364": { "src_dot_components_dot_AssignProductDialog_dot_assignProductDialogContent": {
"string": "Search by product name, attribute, product type etc..."
},
"src_dot_components_dot_AssignProductDialog_dot_2850255786": {
"string": "Search Products" "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", "context": "dialog header",
"string": "Assign Product" "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": { "src_dot_components_dot_AttributeUnassignDialog_dot_2037985699": {
"string": "Are you sure you want to unassign {attributeName} from {itemTypeName}?" "string": "Are you sure you want to unassign {attributeName} from {itemTypeName}?"
}, },
@ -2835,42 +2852,46 @@
"src_dot_discounts": { "src_dot_discounts": {
"string": "Discounts" "string": "Discounts"
}, },
"src_dot_discounts_dot_components_dot_DiscountCategories_dot_1567318211": { "src_dot_discounts_dot_components_dot_DiscountCategories_dot_discountCategoriesButton": {
"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": {
"context": "button", "context": "button",
"string": "Assign categories" "string": "Assign categories"
}, },
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_1035511604": { "src_dot_discounts_dot_components_dot_DiscountCategories_dot_discountCategoriesHeader": {
"context": "button", "context": "section header",
"string": "Assign collections" "string": "Eligible Categories"
}, },
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_2137803833": { "src_dot_discounts_dot_components_dot_DiscountCategories_dot_discountCategoriesNotFound": {
"string": "No collections found" "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", "context": "number of products",
"string": "Products" "string": "Products"
}, },
"src_dot_discounts_dot_components_dot_DiscountCollections_dot_3011396316": { "src_dot_discounts_dot_components_dot_DiscountCollections_dot_discountCollectionsButton": {
"string": "Collection name" "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", "context": "section header",
"string": "Eligible Collections" "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": { "src_dot_discounts_dot_components_dot_DiscountCountrySelectDialog_dot_1585396479": {
"context": "dialog header", "context": "dialog header",
"string": "Assign Countries" "string": "Assign Countries"
@ -2902,26 +2923,53 @@
"context": "time during discount is active, header", "context": "time during discount is active, header",
"string": "Active Dates" "string": "Active Dates"
}, },
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_1657559629": { "src_dot_discounts_dot_components_dot_DiscountProducts_dot_discountProductsButton": {
"string": "No products found"
},
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_2100305525": {
"context": "button", "context": "button",
"string": "Assign products" "string": "Assign products"
}, },
"src_dot_discounts_dot_components_dot_DiscountProducts_dot_2697405188": { "src_dot_discounts_dot_components_dot_DiscountProducts_dot_discountProductsHeader": {
"string": "Product Name" "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", "context": "product availability",
"string": "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" "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", "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": { "src_dot_discounts_dot_components_dot_SaleCreatePage_dot_3866518732": {
"context": "page header", "context": "page header",
@ -3288,44 +3336,74 @@
"src_dot_discounts_dot_views_dot_SaleCreate_dot_480188715": { "src_dot_discounts_dot_views_dot_SaleCreate_dot_480188715": {
"string": "Manage Sales Channel Availability" "string": "Manage Sales Channel Availability"
}, },
"src_dot_discounts_dot_views_dot_SaleDetails_dot_1457489953": { "src_dot_discounts_dot_views_dot_SaleDetails_dot_saleDetailsChannelAvailabilityDialogHeader": {
"context": "dialog content", "context": "channel availability dialog header",
"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": {
"string": "Manage Channel Availability" "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", "context": "dialog header",
"string": "Delete Sale" "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", "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?}}" "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": { "src_dot_discounts_dot_views_dot_SaleList_dot_2809303671": {
"context": "dialog header", "context": "dialog header",
"string": "Delete Sales" "string": "Delete Sales"

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import { useIntl } from "react-intl";
import AssignContainerDialog, { import AssignContainerDialog, {
AssignContainerDialogProps AssignContainerDialogProps
} from "../AssignContainerDialog"; } from "../AssignContainerDialog";
import { messages } from "./messages";
interface AssignCategoryDialogProps interface AssignCategoryDialogProps
extends Omit<AssignContainerDialogProps, "containers" | "title" | "search"> { extends Omit<AssignContainerDialogProps, "containers" | "title" | "search"> {
@ -21,17 +22,12 @@ const AssignCategoryDialog: React.FC<AssignCategoryDialogProps> = ({
<AssignContainerDialog <AssignContainerDialog
containers={categories} containers={categories}
search={{ search={{
label: intl.formatMessage({ label: intl.formatMessage(messages.assignCategoryDialogLabel),
defaultMessage: "Search Category" placeholder: intl.formatMessage(
}), messages.assignCategoryDialogPlaceholder
placeholder: intl.formatMessage({ )
defaultMessage: "Search by category name, etc..."
})
}} }}
title={intl.formatMessage({ title={intl.formatMessage(messages.assignCategoryDialogHeader)}
defaultMessage: "Assign Category",
description: "dialog header"
})}
{...rest} {...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, { import AssignContainerDialog, {
AssignContainerDialogProps AssignContainerDialogProps
} from "../AssignContainerDialog"; } from "../AssignContainerDialog";
import { messages } from "./messages";
interface AssignCollectionDialogProps interface AssignCollectionDialogProps
extends Omit<AssignContainerDialogProps, "containers" | "title" | "search"> { extends Omit<AssignContainerDialogProps, "containers" | "title" | "search"> {
@ -21,17 +22,12 @@ const AssignCollectionDialog: React.FC<AssignCollectionDialogProps> = ({
<AssignContainerDialog <AssignContainerDialog
containers={collections} containers={collections}
search={{ search={{
label: intl.formatMessage({ label: intl.formatMessage(messages.assignCollectionDialogLabel),
defaultMessage: "Search Collection" placeholder: intl.formatMessage(
}), messages.assignCollectionDialogPlaceholder
placeholder: intl.formatMessage({ )
defaultMessage: "Search by collection name, etc..."
})
}} }}
title={intl.formatMessage({ title={intl.formatMessage(messages.assignCollectionDialogHeader)}
defaultMessage: "Assign Collection",
description: "dialog header"
})}
{...rest} {...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 ResponsiveTable from "@saleor/components/ResponsiveTable";
import useSearchQuery from "@saleor/hooks/useSearchQuery"; import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui";
import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle"; import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle";
import { FetchMoreProps, Node } from "@saleor/types"; import { DialogProps, FetchMoreProps, Node } from "@saleor/types";
import React from "react"; import React from "react";
import InfiniteScroll from "react-infinite-scroll-component"; import InfiniteScroll from "react-infinite-scroll-component";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
import ConfirmButton, { ConfirmButtonTransitionState } from "../ConfirmButton"; import ConfirmButton, { ConfirmButtonTransitionState } from "../ConfirmButton";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface FormData { export interface AssignContainerDialogFormData {
containers: string[]; containers: string[];
query: string; query: string;
} }
const useStyles = makeStyles(
{
avatar: {
"&:first-child": {
paddingLeft: 0
}
},
checkboxCell: {
paddingLeft: 0
},
wideCell: {
width: "100%"
}
},
{ name: "AssignContainerDialog" }
);
interface Container extends Node { interface Container extends Node {
name: string; name: string;
} }
export interface AssignContainerDialogProps extends FetchMoreProps { export interface AssignContainerDialogProps
extends FetchMoreProps,
DialogProps {
confirmButtonState: ConfirmButtonTransitionState; confirmButtonState: ConfirmButtonTransitionState;
containers: Container[]; containers: Container[];
loading: boolean; loading: boolean;
open: boolean;
search: Record<"label" | "placeholder", string>; search: Record<"label" | "placeholder", string>;
title: string; title: string;
onClose: () => void;
onFetch: (value: string) => void; onFetch: (value: string) => void;
onSubmit: (data: string[]) => void; onSubmit: (data: string[]) => void;
} }
@ -188,7 +172,7 @@ const AssignContainerDialog: React.FC<AssignContainerDialogProps> = props => {
type="submit" type="submit"
onClick={handleSubmit} onClick={handleSubmit}
> >
<FormattedMessage defaultMessage="Assign" description="button" /> <FormattedMessage {...messages.assignContainerDialogButton} />
</ConfirmButton> </ConfirmButton>
</DialogActions> </DialogActions>
</Dialog> </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 TableCellAvatar from "@saleor/components/TableCellAvatar";
import useSearchQuery from "@saleor/hooks/useSearchQuery"; import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts"; import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts";
import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle"; import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle";
import { FetchMoreProps } from "@saleor/types"; import { DialogProps, FetchMoreProps } from "@saleor/types";
import React from "react"; import React from "react";
import InfiniteScroll from "react-infinite-scroll-component"; import InfiniteScroll from "react-infinite-scroll-component";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface FormData { export interface AssignProductDialogFormData {
products: SearchProducts_search_edges_node[]; products: SearchProducts_search_edges_node[];
query: string; query: string;
} }
const useStyles = makeStyles( export interface AssignProductDialogProps extends FetchMoreProps, DialogProps {
{
avatar: {
"&&:first-child": {
paddingLeft: 0
},
width: 72
},
checkboxCell: {
paddingLeft: 0,
width: 88
},
colName: {
paddingLeft: 0
}
},
{ name: "AssignProductDialog" }
);
export interface AssignProductDialogProps extends FetchMoreProps {
confirmButtonState: ConfirmButtonTransitionState; confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
products: SearchProducts_search_edges_node[]; products: SearchProducts_search_edges_node[];
loading: boolean; loading: boolean;
onClose: () => void;
onFetch: (value: string) => void; onFetch: (value: string) => void;
onSubmit: (data: SearchProducts_search_edges_node[]) => void; onSubmit: (data: string[]) => void;
} }
function handleProductAssign( function handleProductAssign(
product: SearchProducts_search_edges_node, productID: string,
isSelected: boolean, isSelected: boolean,
selectedProducts: SearchProducts_search_edges_node[], selectedProducts: string[],
setSelectedProducts: (data: SearchProducts_search_edges_node[]) => void setSelectedProducts: (data: string[]) => void
) { ) {
if (isSelected) { if (isSelected) {
setSelectedProducts( setSelectedProducts(
selectedProducts.filter( selectedProducts.filter(selectedProduct => selectedProduct !== productID)
selectedProduct => selectedProduct.id !== product.id
)
); );
} else { } else {
setSelectedProducts([...selectedProducts, product]); setSelectedProducts([...selectedProducts, productID]);
} }
} }
@ -98,9 +76,7 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
const intl = useIntl(); const intl = useIntl();
const [query, onQueryChange] = useSearchQuery(onFetch); const [query, onQueryChange] = useSearchQuery(onFetch);
const [selectedProducts, setSelectedProducts] = React.useState< const [selectedProducts, setSelectedProducts] = React.useState<string[]>([]);
SearchProducts_search_edges_node[]
>([]);
const handleSubmit = () => onSubmit(selectedProducts); const handleSubmit = () => onSubmit(selectedProducts);
@ -113,23 +89,15 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
maxWidth="sm" maxWidth="sm"
> >
<DialogTitle> <DialogTitle>
<FormattedMessage <FormattedMessage {...messages.assignVariantDialogHeader} />
defaultMessage="Assign Product"
description="dialog header"
/>
</DialogTitle> </DialogTitle>
<DialogContent className={scrollableDialogClasses.topArea}> <DialogContent className={scrollableDialogClasses.topArea}>
<TextField <TextField
name="query" name="query"
value={query} value={query}
onChange={onQueryChange} onChange={onQueryChange}
label={intl.formatMessage({ label={intl.formatMessage(messages.assignProductDialogSearch)}
defaultMessage: "Search Products" placeholder={intl.formatMessage(messages.assignProductDialogContent)}
})}
placeholder={intl.formatMessage({
defaultMessage:
"Search by product name, attribute, product type etc..."
})}
fullWidth fullWidth
InputProps={{ InputProps={{
autoComplete: "off", autoComplete: "off",
@ -158,7 +126,7 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
{products && {products &&
products.map(product => { products.map(product => {
const isSelected = selectedProducts.some( const isSelected = selectedProducts.some(
selectedProduct => selectedProduct.id === product.id selectedProduct => selectedProduct === product.id
); );
return ( return (
@ -181,7 +149,7 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
checked={isSelected} checked={isSelected}
onChange={() => onChange={() =>
handleProductAssign( handleProductAssign(
product, product.id,
isSelected, isSelected,
selectedProducts, selectedProducts,
setSelectedProducts setSelectedProducts
@ -208,10 +176,7 @@ const AssignProductDialog: React.FC<AssignProductDialogProps> = props => {
type="submit" type="submit"
onClick={handleSubmit} onClick={handleSubmit}
> >
<FormattedMessage <FormattedMessage {...messages.assignProductDialogButton} />
defaultMessage="Assign products"
description="button"
/>
</ConfirmButton> </ConfirmButton>
</DialogActions> </DialogActions>
</Dialog> </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 Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import { makeStyles } from "@saleor/macaw-ui";
import { mapEdgesToItems } from "@saleor/utils/maps"; import { mapEdgesToItems } from "@saleor/utils/maps";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -23,35 +22,14 @@ import { maybe, renderCollection } from "../../../misc";
import { ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "../../../types";
import { SaleDetails_sale } from "../../types/SaleDetails"; import { SaleDetails_sale } from "../../types/SaleDetails";
import { VoucherDetails_voucher } from "../../types/VoucherDetails"; import { VoucherDetails_voucher } from "../../types/VoucherDetails";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface DiscountCategoriesProps extends ListProps, ListActions { export interface DiscountCategoriesProps extends ListProps, ListActions {
discount: SaleDetails_sale | VoucherDetails_voucher; discount: SaleDetails_sale | VoucherDetails_voucher;
onCategoryAssign: () => void; onCategoryAssign: () => void;
onCategoryUnassign: (id: string) => 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 numberOfColumns = 4;
const DiscountCategories: React.FC<DiscountCategoriesProps> = props => { const DiscountCategories: React.FC<DiscountCategoriesProps> = props => {
@ -77,16 +55,10 @@ const DiscountCategories: React.FC<DiscountCategoriesProps> = props => {
return ( return (
<Card> <Card>
<CardTitle <CardTitle
title={intl.formatMessage({ title={intl.formatMessage(messages.discountCategoriesHeader)}
defaultMessage: "Eligible Categories",
description: "section header"
})}
toolbar={ toolbar={
<Button color="primary" onClick={onCategoryAssign}> <Button color="primary" onClick={onCategoryAssign}>
<FormattedMessage <FormattedMessage {...messages.discountCategoriesButton} />
defaultMessage="Assign categories"
description="button"
/>
</Button> </Button>
} }
/> />
@ -107,12 +79,13 @@ const DiscountCategories: React.FC<DiscountCategoriesProps> = props => {
> >
<> <>
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
<FormattedMessage defaultMessage="Category name" /> <FormattedMessage
{...messages.discountCategoriesTableProductHeader}
/>
</TableCell> </TableCell>
<TableCell className={classes.colProducts}> <TableCell className={classes.colProducts}>
<FormattedMessage <FormattedMessage
defaultMessage="Products" {...messages.discountCategoriesTableProductNumber}
description="number of products"
/> />
</TableCell> </TableCell>
<TableCell /> <TableCell />
@ -179,7 +152,7 @@ const DiscountCategories: React.FC<DiscountCategoriesProps> = props => {
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={numberOfColumns}> <TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No categories found" /> <FormattedMessage {...messages.discountCategoriesNotFound} />
</TableCell> </TableCell>
</TableRow> </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 Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import { makeStyles } from "@saleor/macaw-ui";
import { mapEdgesToItems } from "@saleor/utils/maps"; import { mapEdgesToItems } from "@saleor/utils/maps";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -23,36 +22,14 @@ import { maybe, renderCollection } from "../../../misc";
import { ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "../../../types";
import { SaleDetails_sale } from "../../types/SaleDetails"; import { SaleDetails_sale } from "../../types/SaleDetails";
import { VoucherDetails_voucher } from "../../types/VoucherDetails"; import { VoucherDetails_voucher } from "../../types/VoucherDetails";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface DiscountCollectionsProps extends ListProps, ListActions { export interface DiscountCollectionsProps extends ListProps, ListActions {
discount: SaleDetails_sale | VoucherDetails_voucher; discount: SaleDetails_sale | VoucherDetails_voucher;
onCollectionAssign: () => void; onCollectionAssign: () => void;
onCollectionUnassign: (id: string) => 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 numberOfColumns = 4;
const DiscountCollections: React.FC<DiscountCollectionsProps> = props => { const DiscountCollections: React.FC<DiscountCollectionsProps> = props => {
@ -79,16 +56,10 @@ const DiscountCollections: React.FC<DiscountCollectionsProps> = props => {
return ( return (
<Card> <Card>
<CardTitle <CardTitle
title={intl.formatMessage({ title={intl.formatMessage(messages.discountCollectionsHeader)}
defaultMessage: "Eligible Collections",
description: "section header"
})}
toolbar={ toolbar={
<Button color="primary" onClick={onCollectionAssign}> <Button color="primary" onClick={onCollectionAssign}>
<FormattedMessage <FormattedMessage {...messages.discountCollectionsButton} />
defaultMessage="Assign collections"
description="button"
/>
</Button> </Button>
} }
/> />
@ -108,12 +79,13 @@ const DiscountCollections: React.FC<DiscountCollectionsProps> = props => {
toolbar={toolbar} toolbar={toolbar}
> >
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
<FormattedMessage defaultMessage="Collection name" />
</TableCell>
<TableCell className={classes.textRight}>
<FormattedMessage <FormattedMessage
defaultMessage="Products" {...messages.discountCollectionsTableProductHeader}
description="number of products" />
</TableCell>
<TableCell className={classes.colProducts}>
<FormattedMessage
{...messages.discountCollectionsTableProductNumber}
/> />
</TableCell> </TableCell>
<TableCell /> <TableCell />
@ -181,7 +153,7 @@ const DiscountCollections: React.FC<DiscountCollectionsProps> = props => {
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={numberOfColumns}> <TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No collections found" /> <FormattedMessage {...messages.discountCollectionsNotFound} />
</TableCell> </TableCell>
</TableRow> </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 ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import TableCellAvatar from "@saleor/components/TableCellAvatar";
import { AVATAR_MARGIN } from "@saleor/components/TableCellAvatar/Avatar";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import { makeStyles } from "@saleor/macaw-ui";
import { mapEdgesToItems } from "@saleor/utils/maps";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "../../../misc";
import { ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "../../../types";
import { SaleDetails_sale } from "../../types/SaleDetails"; import { SaleDetails_sale_products_edges_node } from "../../types/SaleDetails";
import { VoucherDetails_voucher } from "../../types/VoucherDetails"; import { VoucherDetails_voucher_products_edges_node } from "../../types/VoucherDetails";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface SaleProductsProps extends ListProps, ListActions { export interface SaleProductsProps extends ListProps, ListActions {
discount: SaleDetails_sale | VoucherDetails_voucher; products:
| SaleDetails_sale_products_edges_node[]
| VoucherDetails_voucher_products_edges_node[];
channelsCount: number; channelsCount: number;
onProductAssign: () => void; onProductAssign: () => void;
onProductUnassign: (id: string) => 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 numberOfColumns = 5;
const DiscountProducts: React.FC<SaleProductsProps> = props => { const DiscountProducts: React.FC<SaleProductsProps> = props => {
const { const {
channelsCount, channelsCount,
discount: sale, products,
disabled, disabled,
pageInfo, pageInfo,
onRowClick, onRowClick,
@ -91,20 +60,14 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
return ( return (
<Card> <Card>
<CardTitle <CardTitle
title={intl.formatMessage({ title={intl.formatMessage(messages.discountProductsHeader)}
defaultMessage: "Eligible Products",
description: "section header"
})}
toolbar={ toolbar={
<Button <Button
color="primary" color="primary"
onClick={onProductAssign} onClick={onProductAssign}
data-test-id="assign-products" data-test-id="assign-products"
> >
<FormattedMessage <FormattedMessage {...messages.discountProductsButton} />
defaultMessage="Assign products"
description="button"
/>
</Button> </Button>
} }
/> />
@ -120,22 +83,23 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
colSpan={numberOfColumns} colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={mapEdgesToItems(sale?.products)} items={products}
toggleAll={toggleAll} toggleAll={toggleAll}
toolbar={toolbar} toolbar={toolbar}
> >
<TableCell className={classes.colName}> <TableCell className={classes.colName}>
<span className={classes.colNameLabel}> <span className={products?.length > 0 && classes.colNameLabel}>
<FormattedMessage defaultMessage="Product Name" /> <FormattedMessage
{...messages.discountProductsTableProductHeader}
/>
</span> </span>
</TableCell> </TableCell>
<TableCell className={classes.colType}> <TableCell className={classes.colType}>
<FormattedMessage defaultMessage="Product Type" /> <FormattedMessage {...messages.discountProductsTableTypeHeader} />
</TableCell> </TableCell>
<TableCell className={classes.colPublished}> <TableCell className={classes.colPublished}>
<FormattedMessage <FormattedMessage
defaultMessage="Availability" {...messages.discountProductsTableAvailabilityHeader}
description="product availability"
/> />
</TableCell> </TableCell>
<TableCell className={classes.colActions} /> <TableCell className={classes.colActions} />
@ -155,7 +119,7 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
</TableFooter> </TableFooter>
<TableBody> <TableBody>
{renderCollection( {renderCollection(
mapEdgesToItems(sale?.products), products,
product => { product => {
const isSelected = product ? isChecked(product.id) : false; const isSelected = product ? isChecked(product.id) : false;
@ -216,7 +180,7 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={numberOfColumns}> <TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No products found" /> <FormattedMessage {...messages.discountProductsNotFound} />
</TableCell> </TableCell>
</TableRow> </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 { sectionNames } from "@saleor/intl";
import { Backlink } from "@saleor/macaw-ui"; import { Backlink } from "@saleor/macaw-ui";
import { validatePrice } from "@saleor/products/utils/validation"; import { validatePrice } from "@saleor/products/utils/validation";
import { mapEdgesToItems } from "@saleor/utils/maps";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react"; import React from "react";
@ -30,6 +31,7 @@ import DiscountCategories from "../DiscountCategories";
import DiscountCollections from "../DiscountCollections"; import DiscountCollections from "../DiscountCollections";
import DiscountDates from "../DiscountDates"; import DiscountDates from "../DiscountDates";
import DiscountProducts from "../DiscountProducts"; import DiscountProducts from "../DiscountProducts";
import DiscountVariants from "../DiscountVariants";
import SaleInfo from "../SaleInfo"; import SaleInfo from "../SaleInfo";
import SaleSummary from "../SaleSummary"; import SaleSummary from "../SaleSummary";
import SaleType from "../SaleType"; import SaleType from "../SaleType";
@ -49,20 +51,27 @@ export interface SaleDetailsPageFormData extends MetadataFormData {
export enum SaleDetailsPageTab { export enum SaleDetailsPageTab {
categories = "categories", categories = "categories",
collections = "collections", collections = "collections",
products = "products" products = "products",
variants = "variants"
} }
export function saleDetailsPageTab(tab: string): SaleDetailsPageTab { export function saleDetailsPageTab(tab: string): SaleDetailsPageTab {
return tab === SaleDetailsPageTab.products return tab === SaleDetailsPageTab.products
? SaleDetailsPageTab.products ? SaleDetailsPageTab.products
: tab === SaleDetailsPageTab.collections : tab === SaleDetailsPageTab.collections
? SaleDetailsPageTab.collections ? SaleDetailsPageTab.collections
: SaleDetailsPageTab.categories; : tab === SaleDetailsPageTab.categories
? SaleDetailsPageTab.categories
: SaleDetailsPageTab.variants;
} }
export interface SaleDetailsPageProps export interface SaleDetailsPageProps
extends Pick<ListProps, Exclude<keyof ListProps, "onRowClick">>, extends Pick<ListProps, Exclude<keyof ListProps, "onRowClick">>,
TabListActions< TabListActions<
"categoryListToolbar" | "collectionListToolbar" | "productListToolbar" | "categoryListToolbar"
| "collectionListToolbar"
| "productListToolbar"
| "variantListToolbar"
>, >,
ChannelProps { ChannelProps {
activeTab: SaleDetailsPageTab; activeTab: SaleDetailsPageTab;
@ -82,6 +91,9 @@ export interface SaleDetailsPageProps
onProductAssign: () => void; onProductAssign: () => void;
onProductUnassign: (id: string) => void; onProductUnassign: (id: string) => void;
onProductClick: (id: string) => () => void; onProductClick: (id: string) => () => void;
onVariantAssign: () => void;
onVariantUnassign: (id: string) => void;
onVariantClick: (productId: string, variantId: string) => () => void;
onRemove: () => void; onRemove: () => void;
onSubmit: (data: SaleDetailsPageFormData) => void; onSubmit: (data: SaleDetailsPageFormData) => void;
onTabClick: (index: SaleDetailsPageTab) => void; onTabClick: (index: SaleDetailsPageTab) => void;
@ -92,6 +104,7 @@ export interface SaleDetailsPageProps
const CategoriesTab = Tab(SaleDetailsPageTab.categories); const CategoriesTab = Tab(SaleDetailsPageTab.categories);
const CollectionsTab = Tab(SaleDetailsPageTab.collections); const CollectionsTab = Tab(SaleDetailsPageTab.collections);
const ProductsTab = Tab(SaleDetailsPageTab.products); const ProductsTab = Tab(SaleDetailsPageTab.products);
const VariantsTab = Tab(SaleDetailsPageTab.variants);
const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
activeTab, activeTab,
@ -120,9 +133,13 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
onProductAssign, onProductAssign,
onProductUnassign, onProductUnassign,
onProductClick, onProductClick,
onVariantAssign,
onVariantUnassign,
onVariantClick,
categoryListToolbar, categoryListToolbar,
collectionListToolbar, collectionListToolbar,
productListToolbar, productListToolbar,
variantListToolbar,
isChecked, isChecked,
selected, selected,
selectedChannelId, selectedChannelId,
@ -239,6 +256,25 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
} }
)} )}
</ProductsTab> </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> </TabContainer>
<CardSpacer /> <CardSpacer />
{activeTab === SaleDetailsPageTab.categories ? ( {activeTab === SaleDetailsPageTab.categories ? (
@ -273,7 +309,7 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
toggleAll={toggleAll} toggleAll={toggleAll}
toolbar={collectionListToolbar} toolbar={collectionListToolbar}
/> />
) : ( ) : activeTab === SaleDetailsPageTab.products ? (
<DiscountProducts <DiscountProducts
disabled={disabled} disabled={disabled}
onNextPage={onNextPage} onNextPage={onNextPage}
@ -282,7 +318,7 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
onProductUnassign={onProductUnassign} onProductUnassign={onProductUnassign}
onRowClick={onProductClick} onRowClick={onProductClick}
pageInfo={pageInfo} pageInfo={pageInfo}
discount={sale} products={mapEdgesToItems(sale?.products)}
channelsCount={allChannelsCount} channelsCount={allChannelsCount}
isChecked={isChecked} isChecked={isChecked}
selected={selected} selected={selected}
@ -290,6 +326,22 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
toggleAll={toggleAll} toggleAll={toggleAll}
toolbar={productListToolbar} 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 /> <CardSpacer />
<DiscountDates <DiscountDates

View file

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

View file

@ -424,6 +424,147 @@ export const sale: SaleDetails_sale = {
}, },
totalCount: 4 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", startDate: "2019-01-03",
type: "PERCENTAGE" as SaleType type: "PERCENTAGE" as SaleType
}; };

View file

@ -43,6 +43,70 @@ export interface SaleCataloguesAdd_saleCataloguesAdd_sale_channelListings {
currency: string; 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 { export interface SaleCataloguesAdd_saleCataloguesAdd_sale_products_edges_node_productType {
__typename: "ProductType"; __typename: "ProductType";
id: string; id: string;
@ -174,6 +238,7 @@ export interface SaleCataloguesAdd_saleCataloguesAdd_sale {
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
channelListings: SaleCataloguesAdd_saleCataloguesAdd_sale_channelListings[] | null; channelListings: SaleCataloguesAdd_saleCataloguesAdd_sale_channelListings[] | null;
variants: SaleCataloguesAdd_saleCataloguesAdd_sale_variants | null;
products: SaleCataloguesAdd_saleCataloguesAdd_sale_products | null; products: SaleCataloguesAdd_saleCataloguesAdd_sale_products | null;
categories: SaleCataloguesAdd_saleCataloguesAdd_sale_categories | null; categories: SaleCataloguesAdd_saleCataloguesAdd_sale_categories | null;
collections: SaleCataloguesAdd_saleCataloguesAdd_sale_collections | null; collections: SaleCataloguesAdd_saleCataloguesAdd_sale_collections | null;

View file

@ -43,6 +43,70 @@ export interface SaleCataloguesRemove_saleCataloguesRemove_sale_channelListings
currency: string; 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 { export interface SaleCataloguesRemove_saleCataloguesRemove_sale_products_edges_node_productType {
__typename: "ProductType"; __typename: "ProductType";
id: string; id: string;
@ -174,6 +238,7 @@ export interface SaleCataloguesRemove_saleCataloguesRemove_sale {
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
channelListings: SaleCataloguesRemove_saleCataloguesRemove_sale_channelListings[] | null; channelListings: SaleCataloguesRemove_saleCataloguesRemove_sale_channelListings[] | null;
variants: SaleCataloguesRemove_saleCataloguesRemove_sale_variants | null;
products: SaleCataloguesRemove_saleCataloguesRemove_sale_products | null; products: SaleCataloguesRemove_saleCataloguesRemove_sale_products | null;
categories: SaleCataloguesRemove_saleCataloguesRemove_sale_categories | null; categories: SaleCataloguesRemove_saleCataloguesRemove_sale_categories | null;
collections: SaleCataloguesRemove_saleCataloguesRemove_sale_collections | null; collections: SaleCataloguesRemove_saleCataloguesRemove_sale_collections | null;

View file

@ -36,6 +36,70 @@ export interface SaleDetails_sale_channelListings {
currency: string; 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 { export interface SaleDetails_sale_products_edges_node_productType {
__typename: "ProductType"; __typename: "ProductType";
id: string; id: string;
@ -167,6 +231,7 @@ export interface SaleDetails_sale {
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
channelListings: SaleDetails_sale_channelListings[] | null; channelListings: SaleDetails_sale_channelListings[] | null;
variants: SaleDetails_sale_variants | null;
products: SaleDetails_sale_products | null; products: SaleDetails_sale_products | null;
categories: SaleDetails_sale_categories | null; categories: SaleDetails_sale_categories | null;
collections: SaleDetails_sale_collections | null; collections: SaleDetails_sale_collections | null;

View file

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

View file

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

View file

@ -32,6 +32,32 @@ export const saleDetailsFragment = gql`
${saleFragment} ${saleFragment}
fragment SaleDetailsFragment on Sale { fragment SaleDetailsFragment on Sale {
...SaleFragment ...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) { products(after: $after, before: $before, first: $first, last: $last) {
edges { edges {
node { node {

View file

@ -36,6 +36,70 @@ export interface SaleDetailsFragment_channelListings {
currency: string; 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 { export interface SaleDetailsFragment_products_edges_node_productType {
__typename: "ProductType"; __typename: "ProductType";
id: string; id: string;
@ -167,6 +231,7 @@ export interface SaleDetailsFragment {
startDate: any; startDate: any;
endDate: any | null; endDate: any | null;
channelListings: SaleDetailsFragment_channelListings[] | null; channelListings: SaleDetailsFragment_channelListings[] | null;
variants: SaleDetailsFragment_variants | null;
products: SaleDetailsFragment_products | null; products: SaleDetailsFragment_products | null;
categories: SaleDetailsFragment_categories | null; categories: SaleDetailsFragment_categories | null;
collections: SaleDetailsFragment_collections | null; collections: SaleDetailsFragment_collections | null;

View file

@ -12,11 +12,40 @@ export interface SearchProducts_search_edges_node_thumbnail {
url: string; 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 { export interface SearchProducts_search_edges_node {
__typename: "Product"; __typename: "Product";
id: string; id: string;
name: string; name: string;
thumbnail: SearchProducts_search_edges_node_thumbnail | null; thumbnail: SearchProducts_search_edges_node_thumbnail | null;
variants: (SearchProducts_search_edges_node_variants | null)[] | null;
} }
export interface SearchProducts_search_edges { 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 { thumbnail {
url url
} }
variants {
id
name
sku
channelListings {
channel {
id
isActive
name
currencyCode
}
price {
amount
currency
}
}
}
} }
} }
pageInfo { pageInfo {

View file

@ -1873,15 +1873,46 @@ export const products: SearchProducts_search_edges_node[] = [
thumbnail: { thumbnail: {
__typename: "Image", __typename: "Image",
url: "" url: ""
} },
}, variants: [
{ {
__typename: "Product", __typename: "ProductVariant",
id: "2", id: "UHJvZHVjdFZhcmlhbnQ6MjAz",
name: "Banana Juice", name: "1l",
thumbnail: { sku: "43226647",
__typename: "Image", channelListings: [
url: "" {
} __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) Products (4)
</span> </span>
<span
class="MuiTypography-root-id Tab-root-id MuiTypography-body1-id"
data-test-id="variants-tab"
>
Variants (3)
</span>
</div> </div>
<div <div
class="CardSpacer-spacer-id" 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" class="MuiTableCell-root-id MuiTableCell-head-id DiscountCollections-colName-id"
scope="col" scope="col"
> >
Collection name Collection Name
</th> </th>
<th <th
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCollections-textRight-id" class="MuiTableCell-root-id MuiTableCell-head-id DiscountCollections-colProducts-id"
scope="col" scope="col"
> >
Products Products
@ -89484,6 +89490,12 @@ exports[`Storyshots Views / Discounts / Sale details default 1`] = `
> >
Products (4) Products (4)
</span> </span>
<span
class="MuiTypography-root-id Tab-root-id MuiTypography-body1-id"
data-test-id="variants-tab"
>
Variants (3)
</span>
</div> </div>
<div <div
class="CardSpacer-spacer-id" 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" class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colName-id"
scope="col" scope="col"
> >
Category name Category Name
</th> </th>
<th <th
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colProducts-id" 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) Products (4)
</span> </span>
<span
class="MuiTypography-root-id Tab-root-id MuiTypography-body1-id"
data-test-id="variants-tab"
>
Variants (3)
</span>
</div> </div>
<div <div
class="CardSpacer-spacer-id" 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" class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colName-id"
scope="col" scope="col"
> >
Category name Category Name
</th> </th>
<th <th
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colProducts-id" class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colProducts-id"
@ -92363,6 +92381,12 @@ exports[`Storyshots Views / Discounts / Sale details loading 1`] = `
> >
Products (…) Products (…)
</span> </span>
<span
class="MuiTypography-root-id Tab-root-id MuiTypography-body1-id"
data-test-id="variants-tab"
>
Variants (…)
</span>
</div> </div>
<div <div
class="CardSpacer-spacer-id" 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" class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colName-id"
scope="col" scope="col"
> >
Category name Category Name
</th> </th>
<th <th
class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colProducts-id" class="MuiTableCell-root-id MuiTableCell-head-id DiscountCategories-colProducts-id"
@ -93778,6 +93802,12 @@ exports[`Storyshots Views / Discounts / Sale details products 1`] = `
> >
Products (4) Products (4)
</span> </span>
<span
class="MuiTypography-root-id Tab-root-id MuiTypography-body1-id"
data-test-id="variants-tab"
>
Variants (3)
</span>
</div> </div>
<div <div
class="CardSpacer-spacer-id" class="CardSpacer-spacer-id"

View file

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

View file

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

View file

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