Multichannel (#833)

* add multichannel to configuration view

* create multichannels list view

* create multichannels list view

* add ChannelsCreate view

* update channels in configuration

* add stories

* update default messages

* fix ChannelForm props

* update channels list styles

* update snapshots

* update channel form currency input

* update Channels fragments

* extract messages

* remove tabs from channelsList

* channel details, channel delete modal (#598)

* create Channel details view

* create ChannelDeleteDialog

* add channels delete dialog to channels list

* update messages and types

* fixes after review

* channels availability (#609)

* create Channel details view

* update messages and types

* create ChannelsAvailability component

* create more product channels components

* create channels stories, update fixtures, types

* update product views with channels data

* update schema and snapshots

* update defaultMessages

* update ProductUpdate view

* create ChannelsAvailabilityDropdown component

* add product channels to local storage

* update globalTypes

* Update to new schema and resolve issues

* Update messages

* create deleteChannel mutation

* add channels availability component to product create view

* refactor ProductCreate and ProductUpdate views

* CollectionProducts view cleanup

* add disabled prop to ActionDialog

* use updateChannels mutation in ProductCreate view

* ProductCreate - update submit function

* fixes after review

* update snapshots and messages

Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>

* channels shipping components (#655)

* create PricingCard, OrderValue and OrderWeight components

* create ShippingZoneRatesPage and DeleteShippingRateDialog

* update ChannelsAvailability component

* updates after review

* channels shipping views (#662)

* update ChannelsAvailability component

* updates after review

* create PriceRate views, update types

* create weight rates views

* update shipping views, stories, messages

* update snapshots

* update snapshots

* update useChannels hook

* orders channels components (#698)

* create OrderChannelSectionCard component

* update OrderDetailsPage

* update DraftOrderChannelCard

* update snapshots

* update fixtures

* small change after review, update snapshots

* product pricing (#702)

* update product types

* update Pricing in simple product view

* use productVariantCreate mutation in simple product view

* update snapshots and messages

* handle create variant (#715)

* update product types

* update Pricing in simple product view

* handle product create and update errors

* update snapshots and messages

* fix update and create product handlers

* update pricing types

* channels modal - new styles, search input (#717)

* update product types

* update Pricing in simple product view

* handle product create and update errors

* update pricing types

* add search input in ChannelsAvailabilityDialog

* update ChannelsAvailabilityDialog in all views

* update snapshots

* fix search input label styles

* update toggleAllChannels function

* update variant creator (#724)

* update product types

* update Pricing in simple product view

* handle product create and update errors

* update pricing types

* add search input in ChannelsAvailabilityDialog

* update ChannelsAvailabilityDialog in all views

* update snapshots

* add channelLisitngs to variant creator

* update variant creator price styles

* update product variant creator reducer tests

* update createVariants tests

* update error handling in product variant creator

* add Skip pricing for now option

* use PriceField instead of TextField in ProductVariantCreatorSummary

* create price validation function

* fix errors handling in ProductVariantPrice component

* fixes after review

* Product List - remove publish/unpublish buttons (#727)

* ProductList - remove publish and unpublish buttons

* update messages

* update snapshots, messages

* revert changes in ChannelsAvailabilityDropdown

* products/shipping/discount list settings (#739)

* create ChannelSettingsDialog component

* update snapshots

* ProductList - open settings modal when there is no selected channel

* add settings modal to vouchers list

* add settings dialog to sales list

* add setting modal to shipping list

* update shipping

* update snapshots, messages

* useChannelsSettings - remove selectedChannelSlug

* fix channels update handler in product and shipping view

* messages update in ChannelSettingsDialog

* handle product/discount list when there is no channels

* update onSettingsOpen prop

* collection availability dropdown (#743)

* add availability dropdown to collection products list

* update channelListingProduct fragment name

* update voucher view/components with channels (#746)

* update voucher view/components with channels

* update VoucherSummary, remove defaultCurrency from voucher components

* update snapshots

* move getChannelsVariables func to discounts handlers

* update voucher messages

* sale view/components with channels (#750)

* update sale views with channels

* small fixes in discounts

* order views with channels (#752)

* update draft orders with channels

* add channel activate/deactivate mutations

* remove sort by total in orders list

* add error notification on channel activate/deactivate

* product variants channel select (#755)

* add channels selector to ProductVariants component

* remove selectedChannel from ProductUpdate, update messages and snapshots

* update product fragments

* update translations (#762)

* update translations

* fix translation types

* update messages

* update Availability component (#766)

* update ChannelsAvailability component

* update product fixtures

* update collection and channel fixtures

* ChannelsAvailability - handle errors

* update product handlers

* update ChannelsAvailability styles

* update ProductVariant

* update snapshots

* fix missing things in multichannel  (#785)

* add availability dropdown to discount products list

* fix error handling in shipping components

* update product views and components

* update messages

* update category view/components

* update CategoryProducts styles

* remove defaultCurrency from shipping components

* create ChannelsSelect component

* update channels error handling after review

* another fixes after review

* Add channels to collection views/components (#791)

** update collection components and views

* update create collection view

* update error handling in collection

* remove filter bar from collection list

* update products fragments

* small fix in collection create view

* use collectionFragment in useCatalogSearch

* update defaultMessages and snapshots

* update homepage view/drop defaultCurrency (#801)

* update homepage view

* drop defaultCurrency prop

* fix onChannelChange function in home view

* remove visibility from product list filters

* update export products with channels (#803)

* update ProductExportDialog with channels

* add new channel error code

* remover VISIBLE from product export dialog Financial information

* fix input size in ProductVariantCreatorSummary (#804)

* channels currency code select (#806)

* create select with currency codes

* fix ChannelDeleteDialog

* update defaultMessages, remove unneeded ChannelDetails handlers

* fixes after rebase

* replace channelListing with channelListings

* [multichannel] Update schema]

* Fix product create test

Co-authored-by: AlicjaSzu <alicja.szukuc@gmail.com>
Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>
Co-authored-by: Jakub Majorek <majorek.jakub@gmail.com>
This commit is contained in:
Dominik Żegleń 2020-11-17 17:10:42 +01:00 committed by GitHub
parent 596ba1f63b
commit 6f9060144a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
490 changed files with 53395 additions and 19298 deletions

View file

@ -10,5 +10,6 @@ export const PRODUCTS_SELECTORS = {
firstCategoryItem: "#downshift-0-item-0",
visibleRadioBtn: "[name='isPublished']",
saveBtn: "[data-test='button-bar-confirm']",
confirmationMsg: "[data-test='notification']"
confirmationMsg: "[data-test='notification']",
channelAvailabilityItem: "[data-test='channel-availability-item']"
};

View file

@ -0,0 +1,5 @@
/* eslint-disable sort-keys */
export const BUTTON_SELECTORS = {
back: '[data-test="back"]',
submit: '[data-test="submit"]'
};

View file

@ -1,5 +1,6 @@
import { LEFT_MENU_SELECTORS } from "../elements/account/left-menu/left-menu-selectors";
import { PRODUCTS_SELECTORS } from "../elements/catalog/product-selectors";
import { BUTTON_SELECTORS } from "../elements/shared/button-selectors";
// <reference types="cypress" />
describe("Products", () => {
@ -13,6 +14,8 @@ describe("Products", () => {
.click()
.get(PRODUCTS_SELECTORS.products)
.click()
.get(BUTTON_SELECTORS.submit)
.click()
.get(PRODUCTS_SELECTORS.createProductBtn)
.click()
.get(PRODUCTS_SELECTORS.productNameInput)
@ -33,6 +36,9 @@ describe("Products", () => {
.get(PRODUCTS_SELECTORS.categoryItem)
.first()
.click()
.get(PRODUCTS_SELECTORS.channelAvailabilityItem)
.first()
.click()
.get(PRODUCTS_SELECTORS.visibleRadioBtn)
.first()
.click()

File diff suppressed because it is too large Load diff

60
package-lock.json generated
View file

@ -9496,6 +9496,15 @@
"integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=",
"dev": true
},
"currency-codes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/currency-codes/-/currency-codes-2.1.0.tgz",
"integrity": "sha512-aASwFNP8VjZ0y0PWlSW7c9N/isYTLxK6OCbm7aVuQMk7dWO2zgup9KGiFQgeL9OGL5P/ulvCHcjQizmuEeZXtw==",
"requires": {
"first-match": "~0.0.1",
"nub": "~0.0.0"
}
},
"cyclist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
@ -12260,6 +12269,11 @@
"resolved": "https://registry.npmjs.org/find-with-regex/-/find-with-regex-1.1.3.tgz",
"integrity": "sha512-zkEVQ1H3PIQL/19ADKt1lCQU4QGM3OneiderUcFgn5EgTm/TnoUh7HxPAwP8w/vXxWSLC6KtpbDQpypJ5+majw=="
},
"first-match": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/first-match/-/first-match-0.0.1.tgz",
"integrity": "sha1-pg7GQnAPD0NyNOu37D84JHblQv0="
},
"flat-cache": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
@ -12566,8 +12580,7 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"aproba": {
"version": "1.2.0",
@ -12585,13 +12598,11 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"optional": true
"bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -12604,18 +12615,15 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"core-util-is": {
"version": "1.0.2",
@ -12718,8 +12726,7 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"optional": true
"bundled": true
},
"ini": {
"version": "1.3.5",
@ -12729,7 +12736,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -12742,20 +12748,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"optional": true
"bundled": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -12772,7 +12775,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -12845,8 +12847,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"object-assign": {
"version": "4.1.1",
@ -12856,7 +12857,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -12932,8 +12932,7 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"optional": true
"bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@ -12963,7 +12962,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -12981,7 +12979,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -13020,13 +13017,11 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"optional": true
"bundled": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"optional": true
"bundled": true
}
}
},
@ -17570,6 +17565,11 @@
"boolbase": "~1.0.0"
}
},
"nub": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/nub/-/nub-0.0.0.tgz",
"integrity": "sha1-s2m9Mr3eZq9ZYFw7BSC8IZ3MwE8="
},
"num2fraction": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",

View file

@ -37,6 +37,7 @@
"apollo-upload-client": "^9.1.0",
"classnames": "^2.2.6",
"crc-32": "^1.2.0",
"currency-codes": "^2.1.0",
"downshift": "^1.31.16",
"draft-js": "^0.10.5",
"draftail": "^1.2.1",

File diff suppressed because it is too large Load diff

View file

@ -62,7 +62,7 @@ export const AppsList: React.FC<AppsListProps> = ({ params }) => {
const navigate = useNavigator();
const { updateListSettings, settings } = useListSettings(ListViews.APPS_LIST);
const paginate = usePaginator();
const paginationState = createPaginationState(settings.rowNumber, params);
const paginationState = createPaginationState(settings?.rowNumber, params);
const queryVariables = {
sort: {
direction: OrderDirection.DESC,

View file

@ -35,7 +35,6 @@ export interface AttributeListPageProps
}
const AttributeListPage: React.FC<AttributeListPageProps> = ({
currencySymbol,
filterOpts,
initialSearch,
onAdd,
@ -73,7 +72,6 @@ const AttributeListPage: React.FC<AttributeListPageProps> = ({
defaultMessage: "All Attributes",
description: "tab name"
})}
currencySymbol={currencySymbol}
currentTab={currentTab}
filterStructure={structure}
initialSearch={initialSearch}

View file

@ -19,7 +19,6 @@ import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import createSortHandler from "@saleor/utils/handlers/sortHandler";
@ -52,7 +51,6 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
const navigate = useNavigator();
const paginate = usePaginator();
const notify = useNotifier();
const shop = useShop();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids
);
@ -145,13 +143,11 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
);
const handleSort = createSortHandler(navigate, attributeListUrl, params);
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
return (
<>
<AttributeListPage
attributes={maybe(() => data.attributes.edges.map(edge => edge.node))}
currencySymbol={currencySymbol}
currentTab={currentTab}
disabled={loading || attributeBulkDeleteOpts.loading}
filterOpts={getFilterOpts(params)}

View file

@ -3,11 +3,11 @@ import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import { ChannelsAvailabilityDropdown } from "@saleor/components/ChannelsAvailabilityDropdown";
import Checkbox from "@saleor/components/Checkbox";
import Money from "@saleor/components/Money";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
@ -16,12 +16,9 @@ import TablePagination from "@saleor/components/TablePagination";
import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "@saleor/types";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { FormattedMessage } from "react-intl";
import {
CategoryDetails_category_products_edges_node,
CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted
} from "../../types/CategoryDetails";
import { CategoryDetails_category_products_edges_node } from "../../types/CategoryDetails";
const useStyles = makeStyles(
theme => ({
@ -74,11 +71,14 @@ const useStyles = makeStyles(
);
interface CategoryProductListProps extends ListProps, ListActions {
channelsCount: number;
selectedChannel: string;
products: CategoryDetails_category_products_edges_node[];
}
export const CategoryProductList: React.FC<CategoryProductListProps> = props => {
const {
channelsCount,
disabled,
isChecked,
pageInfo,
@ -89,59 +89,14 @@ export const CategoryProductList: React.FC<CategoryProductListProps> = props =>
toolbar,
onNextPage,
onPreviousPage,
onRowClick
onRowClick,
selectedChannel
} = props;
const classes = useStyles(props);
const intl = useIntl();
const numberOfColumns = 5;
const getProductPrice = (
priceRangeUndiscounted: CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted
) => {
if (!priceRangeUndiscounted) {
return null;
}
const { start, stop } = priceRangeUndiscounted;
const {
gross: { amount: startAmount }
} = start;
const {
gross: { amount: stopAmount }
} = stop;
if (startAmount === stopAmount) {
return (
<Money
money={{
amount: startAmount,
currency: start.gross.currency
}}
/>
);
} else {
return (
<>
<Money
money={{
amount: startAmount,
currency: start.gross.currency
}}
/>
{" - "}
<Money
money={{
amount: stopAmount,
currency: stop.gross.currency
}}
/>
</>
);
}
};
return (
<div className={classes.tableContainer}>
<ResponsiveTable className={classes.table}>
@ -173,8 +128,8 @@ export const CategoryProductList: React.FC<CategoryProductListProps> = props =>
</TableCell>
<TableCell className={classes.colPublished}>
<FormattedMessage
defaultMessage="Published"
description="product status"
defaultMessage="Availability"
description="availability status"
/>
</TableCell>
<TableCell className={classes.colPrice}>
@ -202,6 +157,9 @@ export const CategoryProductList: React.FC<CategoryProductListProps> = props =>
products,
product => {
const isSelected = product ? isChecked(product.id) : false;
const channel = product?.channelListings.find(
listing => listing.channel.id === selectedChannel
);
return (
<TableRow
@ -233,30 +191,21 @@ export const CategoryProductList: React.FC<CategoryProductListProps> = props =>
)}
</TableCell>
<TableCell className={classes.colPublished}>
{product &&
maybe(() => product.isAvailable !== undefined) ? (
<StatusLabel
label={
product.isAvailable
? intl.formatMessage({
defaultMessage: "Published",
description: "product",
id: "productStatusLabel"
})
: intl.formatMessage({
defaultMessage: "Not published",
description: "product"
})
}
status={product.isAvailable ? "success" : "error"}
{product && !product?.channelListings?.length ? (
"-"
) : product?.channelListings !== undefined ? (
<ChannelsAvailabilityDropdown
allChannelsCount={channelsCount}
currentChannel={channel || product?.channelListings[0]}
channels={product?.channelListings}
/>
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colPrice}>
{product?.pricing?.priceRangeUndiscounted ? (
getProductPrice(product?.pricing?.priceRangeUndiscounted)
{product?.channelListings ? (
<Money money={channel?.discountedPrice} />
) : (
<Skeleton />
)}

View file

@ -1,6 +1,11 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import { makeStyles } from "@material-ui/core/styles";
import CardTitle from "@saleor/components/CardTitle";
import { ChannelsSelect } from "@saleor/components/ChannelsSelect";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -10,10 +15,23 @@ import CategoryProductList from "../CategoryProductList";
interface CategoryProductsProps extends PageListProps, ListActions {
products: CategoryDetails_category_products_edges_node[];
channelChoices: SingleAutocompleteChoiceType[];
channelsCount: number;
categoryName: string;
}
const useStyles = makeStyles(
theme => ({
channelsSelectContainer: {
paddingTop: theme.spacing(2)
}
}),
{ name: "CategoryProducts" }
);
export const CategoryProducts: React.FC<CategoryProductsProps> = ({
channelChoices,
channelsCount,
products,
disabled,
pageInfo,
@ -29,6 +47,11 @@ export const CategoryProducts: React.FC<CategoryProductsProps> = ({
toolbar
}) => {
const intl = useIntl();
const classes = useStyles({});
const [channelChoice, setChannelChoice] = useStateFromProps(
channelChoices?.length ? channelChoices[0]?.value : ""
);
return (
<Card>
@ -49,7 +72,16 @@ export const CategoryProducts: React.FC<CategoryProductsProps> = ({
</Button>
}
/>
<CardContent className={classes.channelsSelectContainer}>
<ChannelsSelect
channelChoice={channelChoice}
channelChoices={channelChoices}
setChannelChoice={setChannelChoice}
/>
</CardContent>
<CategoryProductList
channelsCount={channelsCount}
selectedChannel={channelChoice}
products={products}
disabled={disabled}
pageInfo={pageInfo}

View file

@ -9,6 +9,7 @@ import Metadata from "@saleor/components/Metadata/Metadata";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { Tab, TabContainer } from "@saleor/components/Tab";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
@ -48,6 +49,8 @@ export interface CategoryUpdatePageProps
hasPreviousPage: boolean;
};
saveButtonBarState: ConfirmButtonTransitionState;
channelChoices: SingleAutocompleteChoiceType[];
channelsCount: number;
onImageDelete: () => void;
onSubmit: (data: CategoryUpdateData) => SubmitPromise;
onImageUpload(file: File);
@ -66,6 +69,8 @@ const ProductsTab = Tab(CategoryPageTab.products);
export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
changeTab,
channelChoices,
channelsCount,
currentTab,
category,
disabled,
@ -198,6 +203,8 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
)}
{currentTab === CategoryPageTab.products && (
<CategoryProducts
channelsCount={channelsCount}
channelChoices={channelChoices}
categoryName={category?.name}
products={products}
disabled={disabled}

View file

@ -128,31 +128,50 @@ export const category: (
cursor: "YXJyYXljb25uZWN0aW9uOjA=",
node: {
__typename: "Product",
id: "UHJvZHVjdDoyMQ==",
isAvailable: true,
name: "Gardner-Schultz",
pricing: {
__typename: "ProductPricingInfo",
priceRangeUndiscounted: {
__typename: "TaxedMoneyRange",
start: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 3,
currency: "USD"
}
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "123",
name: "Channel1"
},
stop: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 8,
currency: "USD"
}
}
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
},
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "12345",
name: "Channel2"
},
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
}
},
],
id: "UHJvZHVjdDoyMQ==",
name: "Gardner-Schultz",
productType: {
__typename: "ProductType",
id: "UHJvZHVjdFR5cGU6Mw==",
@ -166,31 +185,30 @@ export const category: (
cursor: "YXJyYXljb25uZWN0aW9uOjE=",
node: {
__typename: "Product",
id: "UHJvZHVjdDoyMg==",
isAvailable: true,
name: "James, Martinez and Murray",
pricing: {
__typename: "ProductPricingInfo",
priceRangeUndiscounted: {
__typename: "TaxedMoneyRange",
start: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 3,
currency: "USD"
}
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "523",
name: "Channel1"
},
stop: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 8,
currency: "USD"
}
}
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
}
},
],
id: "UHJvZHVjdDoyMg==",
name: "James, Martinez and Murray",
productType: {
__typename: "ProductType",
id: "UHJvZHVjdFR5cGU6Mw==",
@ -204,31 +222,30 @@ export const category: (
cursor: "YXJyYXljb25uZWN0aW9uOjI=",
node: {
__typename: "Product",
id: "UHJvZHVjdDoyMw==",
isAvailable: true,
name: "Curtis, Joyce and Turner",
pricing: {
__typename: "ProductPricingInfo",
priceRangeUndiscounted: {
__typename: "TaxedMoneyRange",
start: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 3,
currency: "USD"
}
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "1234",
name: "Channel1"
},
stop: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 8,
currency: "USD"
}
}
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
}
},
],
id: "UHJvZHVjdDoyMw==",
name: "Curtis, Joyce and Turner",
productType: {
__typename: "ProductType",
id: "UHJvZHVjdFR5cGU6Mw==",
@ -242,31 +259,30 @@ export const category: (
cursor: "YXJyYXljb25uZWN0aW9uOjM=",
node: {
__typename: "Product",
id: "UHJvZHVjdDoyNA==",
isAvailable: true,
name: "Davis, Brown and Ray",
pricing: {
__typename: "ProductPricingInfo",
priceRangeUndiscounted: {
__typename: "TaxedMoneyRange",
start: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 3,
currency: "USD"
}
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "1235",
name: "Channel1"
},
stop: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 8,
currency: "USD"
}
}
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
}
},
],
id: "UHJvZHVjdDoyNA==",
name: "Davis, Brown and Ray",
productType: {
__typename: "ProductType",
id: "UHJvZHVjdFR5cGU6Mw==",
@ -280,31 +296,30 @@ export const category: (
cursor: "YXJyYXljb25uZWN0aW9uOjQ=",
node: {
__typename: "Product",
id: "UHJvZHVjdDoyNQ==",
isAvailable: true,
name: "Gallegos Ltd",
pricing: {
__typename: "ProductPricingInfo",
priceRangeUndiscounted: {
__typename: "TaxedMoneyRange",
start: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 3,
currency: "USD"
}
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "1236",
name: "Channel1"
},
stop: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 8,
currency: "USD"
}
}
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
}
},
],
id: "UHJvZHVjdDoyNQ==",
name: "Gallegos Ltd",
productType: {
__typename: "ProductType",
id: "UHJvZHVjdFR5cGU6Mw==",
@ -318,31 +333,30 @@ export const category: (
cursor: "YXJyYXljb25uZWN0aW9uOjU=",
node: {
__typename: "Product",
id: "UHJvZHVjdDoyNg==",
isAvailable: true,
name: "Franklin Inc",
pricing: {
__typename: "ProductPricingInfo",
priceRangeUndiscounted: {
__typename: "TaxedMoneyRange",
start: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 3,
currency: "USD"
}
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "1237",
name: "Channel1"
},
stop: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 8,
currency: "USD"
}
}
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
}
},
],
id: "UHJvZHVjdDoyNg==",
name: "Franklin Inc",
productType: {
__typename: "ProductType",
id: "UHJvZHVjdFR5cGU6Mw==",
@ -356,31 +370,30 @@ export const category: (
cursor: "YXJyYXljb25uZWN0aW9uOjY=",
node: {
__typename: "Product",
id: "UHJvZHVjdDoyNw==",
isAvailable: true,
name: "Williams-Taylor",
pricing: {
__typename: "ProductPricingInfo",
priceRangeUndiscounted: {
__typename: "TaxedMoneyRange",
start: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 3,
currency: "USD"
}
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "1238",
name: "Channel1"
},
stop: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 8,
currency: "USD"
}
}
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
}
},
],
id: "UHJvZHVjdDoyNw==",
name: "Williams-Taylor",
productType: {
__typename: "ProductType",
id: "UHJvZHVjdFR5cGU6Mw==",
@ -394,31 +407,30 @@ export const category: (
cursor: "YXJyYXljb25uZWN0aW9uOjc=",
node: {
__typename: "Product",
id: "UHJvZHVjdDoyOA==",
isAvailable: true,
name: "Riddle, Evans and Hicks",
pricing: {
__typename: "ProductPricingInfo",
priceRangeUndiscounted: {
__typename: "TaxedMoneyRange",
start: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 3,
currency: "USD"
}
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "1239",
name: "Channel1"
},
stop: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 8,
currency: "USD"
}
}
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
}
},
],
id: "UHJvZHVjdDoyOA==",
name: "Riddle, Evans and Hicks",
productType: {
__typename: "ProductType",
id: "UHJvZHVjdFR5cGU6Mw==",
@ -432,31 +444,30 @@ export const category: (
cursor: "YXJyYXljb25uZWN0aW9uOjg=",
node: {
__typename: "Product",
id: "UHJvZHVjdDoyOQ==",
isAvailable: true,
name: "Hebert-Sherman",
pricing: {
__typename: "ProductPricingInfo",
priceRangeUndiscounted: {
__typename: "TaxedMoneyRange",
start: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 3,
currency: "USD"
}
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "223",
name: "Channel1"
},
stop: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 8,
currency: "USD"
}
}
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
}
},
],
id: "UHJvZHVjdDoyOQ==",
name: "Hebert-Sherman",
productType: {
__typename: "ProductType",
id: "UHJvZHVjdFR5cGU6Mw==",
@ -470,31 +481,30 @@ export const category: (
cursor: "YXJyYXljb25uZWN0aW9uOjk=",
node: {
__typename: "Product",
id: "UHJvZHVjdDozMA==",
isAvailable: true,
name: "Carter and Sons",
pricing: {
__typename: "ProductPricingInfo",
priceRangeUndiscounted: {
__typename: "TaxedMoneyRange",
start: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 3,
currency: "USD"
}
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "323",
name: "Channel1"
},
stop: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 8,
currency: "USD"
}
}
currency: "USD",
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: false
}
},
],
id: "UHJvZHVjdDozMA==",
name: "Carter and Sons",
productType: {
__typename: "ProductType",
id: "UHJvZHVjdFR5cGU6Mw==",

View file

@ -3,7 +3,7 @@ import {
categoryFragment
} from "@saleor/fragments/categories";
import { pageInfoFragment } from "@saleor/fragments/pageInfo";
import { fragmentMoney } from "@saleor/fragments/products";
import { channelListingProductFragment } from "@saleor/fragments/products";
import makeQuery from "@saleor/hooks/makeQuery";
import gql from "graphql-tag";
@ -49,7 +49,7 @@ export const useRootCategoriesQuery = makeQuery<RootCategories, {}>(
);
export const categoryDetails = gql`
${fragmentMoney}
${channelListingProductFragment}
${categoryFragment}
${categoryDetailsFragment}
${pageInfoFragment}
@ -81,7 +81,6 @@ export const categoryDetails = gql`
node {
id
name
isAvailable
thumbnail {
url
}
@ -89,19 +88,8 @@ export const categoryDetails = gql`
id
name
}
pricing {
priceRangeUndiscounted {
start {
gross {
...Money
}
}
stop {
gross {
...Money
}
}
}
channelListings {
...ChannelListingProductFragment
}
}
}

View file

@ -85,47 +85,37 @@ export interface CategoryDetails_category_products_edges_node_productType {
name: string;
}
export interface CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_start_gross {
export interface CategoryDetails_category_products_edges_node_channelListings_discountedPrice {
__typename: "Money";
amount: number;
currency: string;
}
export interface CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_start {
__typename: "TaxedMoney";
gross: CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_start_gross;
export interface CategoryDetails_category_products_edges_node_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
currencyCode: string;
}
export interface CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_stop_gross {
__typename: "Money";
amount: number;
currency: string;
}
export interface CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_stop {
__typename: "TaxedMoney";
gross: CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_stop_gross;
}
export interface CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted {
__typename: "TaxedMoneyRange";
start: CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_start | null;
stop: CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_stop | null;
}
export interface CategoryDetails_category_products_edges_node_pricing {
__typename: "ProductPricingInfo";
priceRangeUndiscounted: CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted | null;
export interface CategoryDetails_category_products_edges_node_channelListings {
__typename: "ProductChannelListing";
isPublished: boolean;
publicationDate: any | null;
discountedPrice: CategoryDetails_category_products_edges_node_channelListings_discountedPrice | null;
isAvailableForPurchase: boolean | null;
availableForPurchase: any | null;
visibleInListings: boolean;
channel: CategoryDetails_category_products_edges_node_channelListings_channel;
}
export interface CategoryDetails_category_products_edges_node {
__typename: "Product";
id: string;
name: string;
isAvailable: boolean | null;
thumbnail: CategoryDetails_category_products_edges_node_thumbnail | null;
productType: CategoryDetails_category_products_edges_node_productType;
pricing: CategoryDetails_category_products_edges_node_pricing | null;
channelListings: CategoryDetails_category_products_edges_node_channelListings[] | null;
}
export interface CategoryDetails_category_products_edges {

View file

@ -1,6 +1,7 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete";
import { useChannelsList } from "@saleor/channels/queries";
import ActionDialog from "@saleor/components/ActionDialog";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle";
@ -78,6 +79,13 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
variables: { ...paginationState, id }
});
const { data: channelsData } = useChannelsList({});
const channelChoices = channelsData?.channels?.map(channel => ({
label: channel.name,
value: channel.id
}));
const category = data?.category;
if (category === null) {
@ -205,6 +213,8 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
<>
<WindowTitle title={maybe(() => data.category.name)} />
<CategoryUpdatePage
channelsCount={channelsData?.channels?.length}
channelChoices={channelChoices}
changeTab={changeTab}
currentTab={params.activeTab}
category={maybe(() => data.category)}

View file

@ -0,0 +1,27 @@
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import { channelsList } from "../../fixtures";
import ChannelDeleteDialog, {
ChannelDeleteDialogProps
} from "./ChannelDeleteDialog";
const props: ChannelDeleteDialogProps = {
channelsChoices: channelsList.map(channel => ({
label: channel.name,
value: channel.id
})),
confirmButtonState: "default",
onBack: () => undefined,
onClose: () => undefined,
onConfirm: () => undefined,
open: true
};
storiesOf("Views / Channels / Delete channel", module)
.addDecorator(Decorator)
.add("default", () => <ChannelDeleteDialog {...props} />)
.add("without channels to choose", () => (
<ChannelDeleteDialog {...props} channelsChoices={[]} />
));

View file

@ -0,0 +1,96 @@
import Typography from "@material-ui/core/Typography";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import {
Choices,
SingleSelectField
} from "@saleor/components/SingleSelectField";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { buttonMessages } from "@saleor/intl";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useStyles } from "../styles";
export interface ChannelDeleteDialogProps {
channelsChoices: Choices;
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
onBack: () => void;
onClose: () => void;
onConfirm: (targetChannelId: string) => void;
}
const ChannelDeleteDialog: React.FC<ChannelDeleteDialogProps> = ({
channelsChoices = [],
confirmButtonState,
open,
onBack,
onClose,
onConfirm
}) => {
const classes = useStyles({});
const intl = useIntl();
const [choice, setChoice] = useStateFromProps(
!!channelsChoices.length ? channelsChoices[0].value : ""
);
const hasChannels = !!channelsChoices?.length;
return (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={() => (hasChannels ? onConfirm(choice) : onBack())}
title={intl.formatMessage({
defaultMessage: "Delete Channel",
description: "dialog header"
})}
confirmButtonLabel={intl.formatMessage(
hasChannels ? buttonMessages.delete : buttonMessages.ok
)}
variant={hasChannels ? "delete" : "default"}
>
<div>
{hasChannels ? (
<>
<Typography>
<FormattedMessage
defaultMessage="All order information from this channel need to be moved to a different channel. Please select channel orders need to be moved to:."
description="delete channel"
/>
</Typography>
<div className={classes.select}>
<SingleSelectField
choices={channelsChoices}
name="channels"
label={intl.formatMessage({
defaultMessage: "Select Channel",
description: "dialog header"
})}
value={choice}
onChange={e => setChoice(e.target.value)}
/>
</div>
<Typography>
<FormattedMessage
defaultMessage="Deleting channel will delete all product data regarding this channel. Are you sure you want to delete this channel?"
description="delete channel"
/>
</Typography>
</>
) : (
<Typography>
<FormattedMessage
defaultMessage="There is no available channel to move order information to. Please create a channel with same currency so that information can be moved to it."
description="currency channel"
/>
</Typography>
)}
</div>
</ActionDialog>
);
};
ChannelDeleteDialog.displayName = "ChannelDeleteDialog";
export default ChannelDeleteDialog;

View file

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

View file

@ -0,0 +1,25 @@
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import { channelCreateErrors } from "../../fixtures";
import ChannelForm, { ChannelFormProps } from "./ChannelForm";
const props: ChannelFormProps = {
data: {
currencyCode: "euro",
name: "Test",
slug: "test"
},
disabled: false,
errors: [],
onChange: () => undefined
};
storiesOf("Views / Channels / Channel form", module)
.addDecorator(Decorator)
.add("default", () => <ChannelForm {...props} />)
.add("disabled", () => <ChannelForm {...props} disabled={true} />)
.add("with errors", () => (
<ChannelForm {...props} errors={channelCreateErrors} />
));

View file

@ -0,0 +1,162 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import InputAdornment from "@material-ui/core/InputAdornment";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer";
import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType
} from "@saleor/components/SingleAutocompleteSelectField";
import { ChannelErrorFragment } from "@saleor/fragments/types/ChannelErrorFragment";
import useClipboard from "@saleor/hooks/useClipboard";
import { ChangeEvent } from "@saleor/hooks/useForm";
import { FormChange } from "@saleor/hooks/useForm";
import { commonMessages } from "@saleor/intl";
import { getFormErrors } from "@saleor/utils/errors";
import getChannelsErrorMessage from "@saleor/utils/errors/channels";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useStyles } from "../styles";
export interface FormData {
name: string;
currencyCode: string;
slug: string;
}
export interface ChannelFormProps {
data: FormData;
disabled: boolean;
currencyCodes?: SingleAutocompleteChoiceType[];
errors: ChannelErrorFragment[];
selectedCurrencyCode?: string;
onChange: FormChange;
onCurrencyCodeChange?: (event: ChangeEvent) => void;
}
export const ChannelForm: React.FC<ChannelFormProps> = ({
currencyCodes,
data,
disabled,
errors,
selectedCurrencyCode,
onChange,
onCurrencyCodeChange
}) => {
const intl = useIntl();
const [copied, copy] = useClipboard();
const formErrors = getFormErrors<keyof FormData, ChannelErrorFragment>(
["name", "slug", "currencyCode"],
errors
);
const classes = useStyles({});
return (
<>
<Card>
<CardTitle
title={intl.formatMessage(commonMessages.generalInformations)}
/>
<CardContent>
<TextField
error={!!formErrors.name}
helperText={getChannelsErrorMessage(formErrors?.name, intl)}
disabled={disabled}
fullWidth
label={intl.formatMessage({
defaultMessage: "Channel Name",
description: "channel name"
})}
name="name"
value={data.name}
onChange={onChange}
/>
<FormSpacer />
<TextField
error={!!formErrors.slug}
helperText={getChannelsErrorMessage(formErrors?.slug, intl)}
disabled={disabled}
fullWidth
label={intl.formatMessage({
defaultMessage: "Slug",
description: "channel slug"
})}
name="slug"
value={data.slug}
onChange={onChange}
InputProps={{
endAdornment: (
<InputAdornment
className={classes.copyBtn}
position="end"
disableTypography
onClick={() => copy(data.slug)}
>
{copied ? (
<FormattedMessage
defaultMessage="Copied"
description="button"
/>
) : (
<FormattedMessage
defaultMessage="Copy"
description="button"
/>
)}
</InputAdornment>
)
}}
/>
<FormSpacer />
</CardContent>
</Card>
<CardSpacer />
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Channel Settings",
description: "channel settings"
})}
/>
<CardContent>
{!!currencyCodes ? (
<SingleAutocompleteSelectField
allowCustomValues
error={!!formErrors.currencyCode}
helperText={getChannelsErrorMessage(
formErrors?.currencyCode,
intl
)}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "Currency",
description: "channel currency"
})}
choices={currencyCodes}
name="currencyCode"
displayValue={selectedCurrencyCode}
value={selectedCurrencyCode}
onChange={onCurrencyCodeChange}
/>
) : (
<>
<Typography variant="caption" className={classes.label}>
<FormattedMessage
defaultMessage="Selected Currency"
description="selected currency"
/>
</Typography>
<Typography>{data.currencyCode}</Typography>
</>
)}
</CardContent>
</Card>
</>
);
};
ChannelForm.displayName = "ChannelForm";
export default ChannelForm;

View file

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

View file

@ -0,0 +1,26 @@
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import { channelsList } from "../../fixtures";
import ChannelSettingsDialog, {
ChannelSettingsDialogProps
} from "./ChannelSettingsDialog";
const channelsChoices = channelsList.map(channel => ({
label: channel.name,
value: channel.id
}));
const props: ChannelSettingsDialogProps = {
channelsChoices,
confirmButtonState: "default",
defaultChoice: channelsChoices[0]?.value,
onClose: () => undefined,
onConfirm: () => undefined,
open: true
};
storiesOf("Views / Channels / Settings dialog", module)
.addDecorator(Decorator)
.add("default", () => <ChannelSettingsDialog {...props} />);

View file

@ -0,0 +1,71 @@
import Typography from "@material-ui/core/Typography";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import {
Choices,
SingleSelectField
} from "@saleor/components/SingleSelectField";
import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useStyles } from "../styles";
export interface ChannelSettingsDialogProps {
channelsChoices: Choices;
confirmButtonState: ConfirmButtonTransitionState;
defaultChoice: string;
open: boolean;
onClose: () => void;
onConfirm: (choice: string) => void;
}
const ChannelSettingsDialog: React.FC<ChannelSettingsDialogProps> = ({
channelsChoices = [],
confirmButtonState,
defaultChoice,
open,
onClose,
onConfirm
}) => {
const classes = useStyles({});
const intl = useIntl();
const [choice, setChoice] = useState(
defaultChoice || (!!channelsChoices.length ? channelsChoices[0].value : "")
);
return (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={() => onConfirm(choice)}
title={intl.formatMessage({
defaultMessage: "Settings",
description: "dialog header"
})}
>
<div>
<Typography>
<FormattedMessage
defaultMessage="Configure the way information are presented in catalog section of Dashboard."
description="channel settings"
/>
</Typography>
<div className={classes.select}>
<SingleSelectField
choices={channelsChoices}
name="channels"
label={intl.formatMessage({
defaultMessage: "Show prices for",
description: "select label"
})}
value={choice}
onChange={e => setChoice(e.target.value)}
/>
</div>
</div>
</ActionDialog>
);
};
ChannelSettingsDialog.displayName = "ChannelSettingsDialog";
export default ChannelSettingsDialog;

View file

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

View file

@ -0,0 +1,16 @@
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import ChannelStatus, { ChannelStatusProps } from "./ChannelStatus";
const props: ChannelStatusProps = {
disabled: false,
isActive: false,
updateChannelStatus: () => undefined
};
storiesOf("Views / Channels / Channel status", module)
.addDecorator(Decorator)
.add("inactive", () => <ChannelStatus {...props} />)
.add("active", () => <ChannelStatus {...props} isActive={true} />);

View file

@ -0,0 +1,71 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import Typography from "@material-ui/core/Typography";
import CardTitle from "@saleor/components/CardTitle";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useStyles } from "../styles";
export interface ChannelStatusProps {
isActive: boolean;
disabled: boolean;
updateChannelStatus: () => void;
}
export const ChannelStatus: React.FC<ChannelStatusProps> = ({
disabled,
isActive,
updateChannelStatus
}) => {
const intl = useIntl();
const classes = useStyles({});
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Channel Status",
description: "channel status title"
})}
/>
<CardContent>
<Typography variant="caption" className={classes.label}>
<FormattedMessage defaultMessage="Status" description="status" />
</Typography>
<Typography>
{isActive ? (
<FormattedMessage defaultMessage="Active" description="active" />
) : (
<FormattedMessage
defaultMessage="Inactive"
description="inactive"
/>
)}
</Typography>
<Button
color="primary"
className={classes.activeBtn}
disabled={disabled}
onClick={() => updateChannelStatus()}
>
{isActive ? (
<FormattedMessage
defaultMessage="Deactivate"
description="deactivate"
/>
) : (
<FormattedMessage
defaultMessage="Activate"
description="activate"
/>
)}
</Button>
</CardContent>
</Card>
);
};
ChannelStatus.displayName = "ChannelStatus";
export default ChannelStatus;

View file

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

View file

@ -0,0 +1,27 @@
import makeStyles from "@material-ui/core/styles/makeStyles";
export const useStyles = makeStyles(
theme => ({
activeBtn: {
marginLeft: theme.spacing(-1),
marginTop: theme.spacing(1.5)
},
copyBtn: {
color: theme.palette.primary.main,
fontSize: 14,
fontWeight: 500,
textTransform: "uppercase"
},
currencyTitle: {
marginBottom: theme.spacing(1)
},
label: {
color: theme.palette.text.secondary
},
select: {
marginBottom: theme.spacing(2),
marginTop: theme.spacing(2)
}
}),
{ name: "ChannelComponents" }
);

164
src/channels/fixtures.ts Normal file
View file

@ -0,0 +1,164 @@
import { ChannelErrorFragment } from "@saleor/fragments/types/ChannelErrorFragment";
import { ProductDetails_product_channelListings } from "@saleor/products/types/ProductDetails";
import { ChannelErrorCode } from "@saleor/types/globalTypes";
import { Channel_channel } from "./types/Channel";
import { Channels_channels } from "./types/Channels";
export const channelCreateErrors: ChannelErrorFragment[] = [
{
__typename: "ChannelError",
code: ChannelErrorCode.UNIQUE,
field: "slug",
message: "Channel with this Slug already exists."
}
];
export const channelsList: Channels_channels[] = [
{
__typename: "Channel",
currencyCode: "euro",
id: "Q2hhbm5lcDoy",
isActive: true,
name: "Test",
slug: "test"
},
{
__typename: "Channel",
currencyCode: "euro",
id: "Q2hhbm7lbDoy213",
isActive: true,
name: "Channel",
slug: "channel"
},
{
__typename: "Channel",
currencyCode: "euro",
id: "Q2hhbn5lbDoytr",
isActive: true,
name: "Channel test",
slug: "channeltest"
},
{
__typename: "Channel",
currencyCode: "euro",
id: "Q2hhbm5lbDo5bot",
isActive: true,
name: "Channel USD",
slug: "channel-usd"
},
{
__typename: "Channel",
currencyCode: "euro",
id: "Q2hhbm7lbDoyr0tr",
isActive: true,
name: "Channel",
slug: "channel2"
},
{
__typename: "Channel",
currencyCode: "euro",
id: "Q2hhbn5lbDoyya",
isActive: true,
name: "Channel test",
slug: "channeltest4"
},
{
__typename: "Channel",
currencyCode: "euro",
id: "Q2hhbm5lbDo5w0z",
isActive: true,
name: "Channel USD",
slug: "channel-usd1"
}
];
export const channel: Channel_channel = {
__typename: "Channel",
currencyCode: "zl",
id: "Q2hhbm5lbDov78",
isActive: true,
name: "Test",
slug: "test"
};
export const productChannels: ProductDetails_product_channelListings[] = [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "123",
name: "Channel1"
},
discountedPrice: {
__typename: "Money",
amount: 5,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: true,
publicationDate: "2020-07-14",
visibleInListings: true
},
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "124",
name: "Channel2"
},
discountedPrice: {
__typename: "Money",
amount: 0,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: "2020-07-30",
visibleInListings: true
},
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "125",
name: "Channel3"
},
discountedPrice: {
__typename: "Money",
amount: 8,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: null,
visibleInListings: true
}
];
export const productPriceChannels = [
{
costPrice: "5",
id: "123",
name: "Channel1",
sellingPrice: "10"
},
{
costPrice: "15",
id: "124",
name: "Channel2",
sellingPrice: "20"
},
{
costPrice: "15",
id: "125",
name: "Channel3",
sellingPrice: "100"
}
];

47
src/channels/index.tsx Normal file
View file

@ -0,0 +1,47 @@
import { sectionNames } from "@saleor/intl";
import { asSortParams } from "@saleor/utils/sort";
import { parse as parseQs } from "qs";
import React from "react";
import { useIntl } from "react-intl";
import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { WindowTitle } from "../components/WindowTitle";
import {
channelAddPath,
channelPath,
channelsListPath,
ChannelsListUrlQueryParams,
ChannelsListUrlSortField
} from "./urls";
import ChannelCreateComponent from "./views/ChannelCreate";
import ChannelDetailsComponent from "./views/ChannelDetails";
import ChannelsListComponent from "./views/ChannelsList";
const ChannelDetails: React.FC<RouteComponentProps<{ id: string }>> = ({
match
}) => <ChannelDetailsComponent id={decodeURIComponent(match.params.id)} />;
const ChannelsList: React.FC<RouteComponentProps> = ({ location }) => {
const qs = parseQs(location.search.substr(1));
const params: ChannelsListUrlQueryParams = asSortParams(
qs,
ChannelsListUrlSortField
);
return <ChannelsListComponent params={params} />;
};
export const ChannelsSection: React.FC<{}> = () => {
const intl = useIntl();
return (
<>
<WindowTitle title={intl.formatMessage(sectionNames.channels)} />
<Switch>
<Route exact path={channelsListPath} component={ChannelsList} />
<Route exact path={channelAddPath} component={ChannelCreateComponent} />
<Route exact path={channelPath(":id")} component={ChannelDetails} />
</Switch>
</>
);
};
export default ChannelsSection;

117
src/channels/mutations.ts Normal file
View file

@ -0,0 +1,117 @@
import {
channelDetailsFragment,
channelErrorFragment
} from "@saleor/fragments/channels";
import makeMutation from "@saleor/hooks/makeMutation";
import gql from "graphql-tag";
import {
ChannelActivate,
ChannelActivateVariables
} from "./types/ChannelActivate";
import { ChannelCreate, ChannelCreateVariables } from "./types/ChannelCreate";
import {
ChannelDeactivate,
ChannelDeactivateVariables
} from "./types/ChannelDeactivate";
import { ChannelDelete, ChannelDeleteVariables } from "./types/ChannelDelete";
import { ChannelUpdate, ChannelUpdateVariables } from "./types/ChannelUpdate";
export const channelCreateMutation = gql`
${channelErrorFragment}
${channelDetailsFragment}
mutation ChannelCreate($input: ChannelCreateInput!) {
channelCreate(input: $input) {
channel {
...ChannelDetailsFragment
}
errors: channelErrors {
...ChannelErrorFragment
}
}
}
`;
export const channelUpdateMutation = gql`
${channelErrorFragment}
${channelDetailsFragment}
mutation ChannelUpdate($id: ID!, $input: ChannelUpdateInput!) {
channelUpdate(id: $id, input: $input) {
channel {
...ChannelDetailsFragment
}
errors: channelErrors {
...ChannelErrorFragment
}
}
}
`;
export const channelDeleteMutation = gql`
${channelErrorFragment}
${channelDetailsFragment}
mutation ChannelDelete($id: ID!, $input: ChannelDeleteInput!) {
channelDelete(id: $id, input: $input) {
channel {
...ChannelDetailsFragment
}
errors: channelErrors {
...ChannelErrorFragment
}
}
}
`;
export const channelActivateMutation = gql`
${channelErrorFragment}
${channelDetailsFragment}
mutation ChannelActivate($id: ID!) {
channelActivate(id: $id) {
channel {
...ChannelDetailsFragment
}
errors: channelErrors {
...ChannelErrorFragment
}
}
}
`;
export const channelDeactivateMutation = gql`
${channelErrorFragment}
${channelDetailsFragment}
mutation ChannelDeactivate($id: ID!) {
channelDeactivate(id: $id) {
channel {
...ChannelDetailsFragment
}
errors: channelErrors {
...ChannelErrorFragment
}
}
}
`;
export const useChannelCreateMutation = makeMutation<
ChannelCreate,
ChannelCreateVariables
>(channelCreateMutation);
export const useChannelUpdateMutation = makeMutation<
ChannelUpdate,
ChannelUpdateVariables
>(channelUpdateMutation);
export const useChannelDeleteMutation = makeMutation<
ChannelDelete,
ChannelDeleteVariables
>(channelDeleteMutation);
export const useChannelActivateMutation = makeMutation<
ChannelActivate,
ChannelActivateVariables
>(channelActivateMutation);
export const useChannelDeactivateMutation = makeMutation<
ChannelDeactivate,
ChannelDeactivateVariables
>(channelDeactivateMutation);

View file

@ -0,0 +1,41 @@
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import { channel, channelCreateErrors } from "../../fixtures";
import ChannelDetailsPage, {
ChannelDetailsPageProps
} from "./ChannelDetailsPage";
const props: ChannelDetailsPageProps = {
currencyCodes: [
{ label: "USD", value: "USD" },
{ label: "PLN", value: "PLN" }
],
disabled: false,
disabledStatus: false,
errors: [],
onBack: () => undefined,
onSubmit: () => undefined,
saveButtonBarState: "default",
updateChannelStatus: () => undefined
};
storiesOf("Views / Channels / Channel details", module)
.addDecorator(Decorator)
.add("default", () => <ChannelDetailsPage {...props} />)
.add("disabled", () => <ChannelDetailsPage {...props} disabled={true} />)
.add("loading", () => (
<ChannelDetailsPage {...props} saveButtonBarState={"loading"} />
))
.add("with data", () => <ChannelDetailsPage {...props} channel={channel} />)
.add("without editable currency code", () => (
<ChannelDetailsPage
{...props}
currencyCodes={undefined}
channel={channel}
/>
))
.add("with errors", () => (
<ChannelDetailsPage {...props} errors={channelCreateErrors} />
));

View file

@ -0,0 +1,93 @@
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { ChannelErrorFragment } from "@saleor/fragments/types/ChannelErrorFragment";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import React from "react";
import { ChannelForm, FormData } from "../../components/ChannelForm";
import { ChannelStatus } from "../../components/ChannelStatus/ChannelStatus";
import { Channel_channel } from "../../types/Channel";
export interface ChannelDetailsPageProps {
channel?: Channel_channel;
currencyCodes?: SingleAutocompleteChoiceType[];
disabled: boolean;
disabledStatus?: boolean;
errors: ChannelErrorFragment[];
saveButtonBarState: ConfirmButtonTransitionState;
onBack?: () => void;
onSubmit?: (data: FormData) => void;
updateChannelStatus?: () => void;
}
const initialData: FormData = {
currencyCode: "",
name: "",
slug: ""
};
export const ChannelDetailsPage: React.FC<ChannelDetailsPageProps> = ({
channel,
currencyCodes,
disabled,
disabledStatus,
errors,
onBack,
onSubmit,
saveButtonBarState,
updateChannelStatus
}) => {
const [selectedCurrencyCode, setSelectedCurrencyCode] = React.useState("");
return (
<Form onSubmit={onSubmit} initial={channel || initialData}>
{({ change, data, hasChanged, submit }) => {
const handleCurrencyCodeSelect = createSingleAutocompleteSelectHandler(
change,
setSelectedCurrencyCode,
currencyCodes
);
const formDisabled = !data.name || !data.slug || !data.currencyCode;
return (
<>
<Grid>
<div>
<ChannelForm
data={data}
disabled={disabled}
currencyCodes={currencyCodes}
selectedCurrencyCode={selectedCurrencyCode}
onChange={change}
onCurrencyCodeChange={handleCurrencyCodeSelect}
errors={errors}
/>
</div>
{!!updateChannelStatus && (
<div>
<ChannelStatus
isActive={channel?.isActive}
disabled={disabledStatus}
updateChannelStatus={updateChannelStatus}
/>
</div>
)}
</Grid>
<SaveButtonBar
onCancel={onBack}
onSave={submit}
state={saveButtonBarState}
disabled={disabled || formDisabled || !onSubmit || !hasChanged}
/>
</>
);
}}
</Form>
);
};
ChannelDetailsPage.displayName = "ChannelDetailsPage";
export default ChannelDetailsPage;

View file

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

View file

@ -0,0 +1,19 @@
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import { channelsList } from "../../fixtures";
import ChannelsListPage, { ChannelsListPageProps } from "./ChannelsListPage";
const props: ChannelsListPageProps = {
channelsList,
navigateToChannelCreate: () => undefined,
onBack: () => undefined,
onRemove: () => undefined,
onRowClick: () => undefined
};
storiesOf("Views / Channels / Channels list", module)
.addDecorator(Decorator)
.add("default", () => <ChannelsListPage {...props} />)
.add("empty", () => <ChannelsListPage {...props} channelsList={[]} />);

View file

@ -0,0 +1,126 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import IconButton from "@material-ui/core/IconButton";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import DeleteIcon from "@material-ui/icons/Delete";
import AppHeader from "@saleor/components/AppHeader";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import TableCellHeader from "@saleor/components/TableCellHeader";
import { sectionNames } from "@saleor/intl";
import { renderCollection, stopPropagation } from "@saleor/misc";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Channels_channels } from "../../types/Channels";
import { useStyles } from "./styles";
export interface ChannelsListPageProps {
channelsList: Channels_channels[] | undefined;
navigateToChannelCreate: () => void;
onBack: () => void;
onRowClick: (id: string) => () => void;
onRemove: (id: string) => void;
}
const numberOfColumns = 2;
export const ChannelsListPage: React.FC<ChannelsListPageProps> = ({
channelsList,
navigateToChannelCreate,
onBack,
onRemove,
onRowClick
}) => {
const intl = useIntl();
const classes = useStyles({});
return (
<Container>
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.configuration)}
</AppHeader>
<PageHeader title={intl.formatMessage(sectionNames.channels)}>
<Button
onClick={navigateToChannelCreate}
color="primary"
variant="contained"
data-test="add-channel"
>
<FormattedMessage
defaultMessage="Create Channel"
description="button"
/>
</Button>
</PageHeader>
<Card>
<ResponsiveTable>
<TableHead>
<TableRow>
<TableCellHeader>
<FormattedMessage
defaultMessage="Channel Name"
description="channel name"
/>
</TableCellHeader>
<TableCell className={classes.colRight}>
<FormattedMessage
defaultMessage="Actions"
description="table actions"
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{renderCollection(
channelsList,
channel => (
<TableRow
hover={!!channel}
key={channel ? channel.id : "skeleton"}
className={classes.tableRow}
onClick={!!channel ? onRowClick(channel.id) : undefined}
>
<TableCell className={classes.colName}>
<span data-test="name">
{channel?.name || <Skeleton />}
</span>
</TableCell>
<TableCell className={classes.colAction}>
{channelsList?.length > 1 && (
<IconButton
color="primary"
onClick={
channel
? stopPropagation(() => onRemove(channel.id))
: undefined
}
>
<DeleteIcon />
</IconButton>
)}
</TableCell>
</TableRow>
),
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage defaultMessage="No channels found" />
</TableCell>
</TableRow>
)
)}
</TableBody>
</ResponsiveTable>
</Card>
</Container>
);
};
ChannelsListPage.displayName = "ChannelsListPage";
export default ChannelsListPage;

View file

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

View file

@ -0,0 +1,37 @@
import makeStyles from "@material-ui/core/styles/makeStyles";
export const useStyles = makeStyles(
theme => ({
[theme.breakpoints.up("lg")]: {
colName: {
"&&": {
width: "auto"
}
}
},
colAction: {
"&&": {
paddingRight: theme.spacing(1)
},
textAlign: "right",
width: 140
},
colName: {
paddingLeft: 0,
width: 250
},
colRight: {
textAlign: "right"
},
columnPicker: {
marginRight: theme.spacing(3)
},
table: {
tableLayout: "fixed"
},
tableRow: {
cursor: "pointer"
}
}),
{ name: "ChannelsListPage" }
);

29
src/channels/queries.ts Normal file
View file

@ -0,0 +1,29 @@
import { channelDetailsFragment } from "@saleor/fragments/channels";
import makeQuery from "@saleor/hooks/makeQuery";
import gql from "graphql-tag";
import { Channel, ChannelVariables } from "./types/Channel";
import { Channels } from "./types/Channels";
export const channelsList = gql`
${channelDetailsFragment}
query Channels {
channels {
...ChannelDetailsFragment
}
}
`;
export const channelDetails = gql`
${channelDetailsFragment}
query Channel($id: ID!) {
channel(id: $id) {
...ChannelDetailsFragment
}
}
`;
export const useChannelsList = makeQuery<Channels, {}>(channelsList);
export const useChannelDetails = makeQuery<Channel, ChannelVariables>(
channelDetails
);

View file

@ -0,0 +1,24 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: Channel
// ====================================================
export interface Channel_channel {
__typename: "Channel";
id: string;
isActive: boolean;
name: string;
slug: string;
currencyCode: string;
}
export interface Channel {
channel: Channel_channel | null;
}
export interface ChannelVariables {
id: string;
}

View file

@ -0,0 +1,39 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ChannelErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ChannelActivate
// ====================================================
export interface ChannelActivate_channelActivate_channel {
__typename: "Channel";
id: string;
isActive: boolean;
name: string;
slug: string;
currencyCode: string;
}
export interface ChannelActivate_channelActivate_errors {
__typename: "ChannelError";
code: ChannelErrorCode;
field: string | null;
message: string | null;
}
export interface ChannelActivate_channelActivate {
__typename: "ChannelActivate";
channel: ChannelActivate_channelActivate_channel | null;
errors: ChannelActivate_channelActivate_errors[];
}
export interface ChannelActivate {
channelActivate: ChannelActivate_channelActivate | null;
}
export interface ChannelActivateVariables {
id: string;
}

View file

@ -0,0 +1,39 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ChannelCreateInput, ChannelErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ChannelCreate
// ====================================================
export interface ChannelCreate_channelCreate_channel {
__typename: "Channel";
id: string;
isActive: boolean;
name: string;
slug: string;
currencyCode: string;
}
export interface ChannelCreate_channelCreate_errors {
__typename: "ChannelError";
code: ChannelErrorCode;
field: string | null;
message: string | null;
}
export interface ChannelCreate_channelCreate {
__typename: "ChannelCreate";
channel: ChannelCreate_channelCreate_channel | null;
errors: ChannelCreate_channelCreate_errors[];
}
export interface ChannelCreate {
channelCreate: ChannelCreate_channelCreate | null;
}
export interface ChannelCreateVariables {
input: ChannelCreateInput;
}

View file

@ -0,0 +1,39 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ChannelErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ChannelDeactivate
// ====================================================
export interface ChannelDeactivate_channelDeactivate_channel {
__typename: "Channel";
id: string;
isActive: boolean;
name: string;
slug: string;
currencyCode: string;
}
export interface ChannelDeactivate_channelDeactivate_errors {
__typename: "ChannelError";
code: ChannelErrorCode;
field: string | null;
message: string | null;
}
export interface ChannelDeactivate_channelDeactivate {
__typename: "ChannelDeactivate";
channel: ChannelDeactivate_channelDeactivate_channel | null;
errors: ChannelDeactivate_channelDeactivate_errors[];
}
export interface ChannelDeactivate {
channelDeactivate: ChannelDeactivate_channelDeactivate | null;
}
export interface ChannelDeactivateVariables {
id: string;
}

View file

@ -0,0 +1,40 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ChannelDeleteInput, ChannelErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ChannelDelete
// ====================================================
export interface ChannelDelete_channelDelete_channel {
__typename: "Channel";
id: string;
isActive: boolean;
name: string;
slug: string;
currencyCode: string;
}
export interface ChannelDelete_channelDelete_errors {
__typename: "ChannelError";
code: ChannelErrorCode;
field: string | null;
message: string | null;
}
export interface ChannelDelete_channelDelete {
__typename: "ChannelDelete";
channel: ChannelDelete_channelDelete_channel | null;
errors: ChannelDelete_channelDelete_errors[];
}
export interface ChannelDelete {
channelDelete: ChannelDelete_channelDelete | null;
}
export interface ChannelDeleteVariables {
id: string;
input: ChannelDeleteInput;
}

View file

@ -0,0 +1,15 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: ChannelDetailsFragment
// ====================================================
export interface ChannelDetailsFragment {
__typename: "Channel";
id: string;
name: string;
slug: string;
currencyCode: string;
}

View file

@ -0,0 +1,16 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ChannelErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: ChannelErrorFragment
// ====================================================
export interface ChannelErrorFragment {
__typename: "ChannelError";
code: ChannelErrorCode;
field: string | null;
message: string | null;
}

View file

@ -0,0 +1,40 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ChannelUpdateInput, ChannelErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ChannelUpdate
// ====================================================
export interface ChannelUpdate_channelUpdate_channel {
__typename: "Channel";
id: string;
isActive: boolean;
name: string;
slug: string;
currencyCode: string;
}
export interface ChannelUpdate_channelUpdate_errors {
__typename: "ChannelError";
code: ChannelErrorCode;
field: string | null;
message: string | null;
}
export interface ChannelUpdate_channelUpdate {
__typename: "ChannelUpdate";
channel: ChannelUpdate_channelUpdate_channel | null;
errors: ChannelUpdate_channelUpdate_errors[];
}
export interface ChannelUpdate {
channelUpdate: ChannelUpdate_channelUpdate | null;
}
export interface ChannelUpdateVariables {
id: string;
input: ChannelUpdateInput;
}

View file

@ -0,0 +1,20 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: Channels
// ====================================================
export interface Channels_channels {
__typename: "Channel";
id: string;
isActive: boolean;
name: string;
slug: string;
currencyCode: string;
}
export interface Channels {
channels: Channels_channels[] | null;
}

33
src/channels/urls.ts Normal file
View file

@ -0,0 +1,33 @@
import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join";
import { Dialog, Filters, SingleAction, Sort } from "../types";
export enum ChannelsListUrlFiltersEnum {
query = "query"
}
export enum ChannelsListUrlSortField {
name = "name"
}
export type ChannelsListUrlSort = Sort<ChannelsListUrlSortField>;
export type ChannelsListUrlFilters = Filters<ChannelsListUrlFiltersEnum>;
export type ChannelsListUrlDialog = "remove";
export type ChannelsListUrlQueryParams = Dialog<ChannelsListUrlDialog> &
ChannelsListUrlFilters &
ChannelsListUrlSort &
SingleAction;
export const channelsSection = "/channels/";
export const channelsListPath = channelsSection;
export const channelsListUrl = (params?: ChannelsListUrlQueryParams) =>
channelsListPath + "?" + stringifyQs(params);
export const channelAddPath = urlJoin(channelsSection, "add");
export const channelAddUrl = channelAddPath;
export const channelPath = (id: string) => urlJoin(channelsSection, id);
export const channelUrl = (id: string, params?: ChannelsListUrlQueryParams) =>
channelPath(encodeURIComponent(id)) + "?" + stringifyQs(params);

306
src/channels/utils.ts Normal file
View file

@ -0,0 +1,306 @@
import { Channels_channels } from "@saleor/channels/types/Channels";
import { CollectionDetails_collection } from "@saleor/collections/types/CollectionDetails";
import { SaleDetails_sale } from "@saleor/discounts/types/SaleDetails";
import { VoucherDetails_voucher } from "@saleor/discounts/types/VoucherDetails";
import { RequireOnlyOne } from "@saleor/misc";
import { ProductDetails_product } from "@saleor/products/types/ProductDetails";
import { ProductVariantDetails_productVariant } from "@saleor/products/types/ProductVariantDetails";
import { ShippingZone_shippingZone_shippingMethods_channelListings } from "@saleor/shipping/types/ShippingZone";
import { uniqBy } from "lodash";
export interface Channel {
id: string;
name: string;
}
export interface ChannelData {
id: string;
isPublished: boolean;
name: string;
publicationDate: string | null;
currency: string;
price: string;
costPrice: string;
availableForPurchase: string;
isAvailableForPurchase: boolean;
visibleInListings: boolean;
}
export interface ChannelPriceData {
id: string;
name: string;
currency: string;
price: string;
costPrice?: string;
}
export interface IChannelPriceArgs {
price: string;
costPrice: string;
}
export type ChannelPriceArgs = RequireOnlyOne<
IChannelPriceArgs,
"price" | "costPrice"
>;
export interface ChannelVoucherData {
id: string;
name: string;
discountValue: string;
currency: string;
minSpent: string;
}
export interface ChannelSaleData {
id: string;
name: string;
discountValue: string;
currency: string;
}
export interface ChannelCollectionData {
id: string;
isPublished: boolean;
name: string;
publicationDate: string | null;
}
export const createCollectionChannels = (data?: Channels_channels[]) =>
data?.map(channel => ({
id: channel.id,
isPublished: false,
name: channel.name,
publicationDate: null
}));
export const createVoucherChannels = (data?: Channels_channels[]) =>
data?.map(channel => ({
currency: channel.currencyCode,
discountValue: "",
id: channel.id,
minSpent: "",
name: channel.name
}));
export const createSaleChannels = (data?: Channels_channels[]) =>
data?.map(channel => ({
currency: channel.currencyCode,
discountValue: "",
id: channel.id,
name: channel.name
}));
export const createVariantChannels = (
data?: ProductVariantDetails_productVariant
): ChannelPriceData[] => {
if (data) {
const productChannels = data?.product.channelListings.map(listing => ({
costPrice: "",
currency: listing.channel.currencyCode,
id: listing.channel.id,
name: listing.channel.name,
price: ""
}));
const variantChannels = data?.channelListings.map(listing => ({
costPrice: listing.costPrice?.amount.toString() || "",
currency: listing.channel.currencyCode,
id: listing.channel.id,
name: listing.channel.name,
price: listing.price.amount.toString()
}));
return uniqBy([...variantChannels, ...productChannels], obj => obj.id);
}
return [];
};
export const createChannelsDataWithSaleDiscountPrice = (
saleData?: SaleDetails_sale,
data?: Channels_channels[]
): ChannelSaleData[] => {
if (data && saleData?.channelListings) {
const dataArr = createSaleChannels(data);
const saleDataArr = createChannelsDataFromSale(saleData);
return uniqBy([...saleDataArr, ...dataArr], obj => obj.id);
}
return [];
};
export const createChannelsDataWithDiscountPrice = (
voucherData?: VoucherDetails_voucher,
data?: Channels_channels[]
): ChannelVoucherData[] => {
if (data && voucherData?.channelListings) {
const dataArr = createVoucherChannels(data);
const voucherDataArr = createChannelsDataFromVoucher(voucherData);
return uniqBy([...voucherDataArr, ...dataArr], obj => obj.id);
}
return [];
};
export const createChannelsData = (data?: Channels_channels[]): ChannelData[] =>
data?.map(channel => ({
availableForPurchase: null,
costPrice: "",
currency: channel.currencyCode,
id: channel.id,
isAvailableForPurchase: false,
isPublished: false,
name: channel.name,
price: "",
publicationDate: null,
visibleInListings: false
})) || [];
export const createChannelsDataWithPrice = (
productData?: ProductDetails_product,
data?: Channels_channels[]
): ChannelData[] => {
if (data && productData?.channelListings) {
const dataArr = createChannelsData(data);
const productDataArr = createChannelsDataFromProduct(productData);
return uniqBy([...productDataArr, ...dataArr], obj => obj.id);
}
return [];
};
export const createShippingChannels = (
data?: Channels_channels[]
): ChannelShippingData[] =>
data?.map(channel => ({
currency: channel.currencyCode,
id: channel.id,
maxValue: "",
minValue: "",
name: channel.name,
price: ""
})) || [];
export const createShippingChannelsFromRate = (
data?: ShippingZone_shippingZone_shippingMethods_channelListings[]
): ChannelShippingData[] =>
data?.map(channelData => ({
currency: channelData.channel.currencyCode,
id: channelData.channel.id,
maxValue: channelData.maximumOrderPrice
? channelData.maximumOrderPrice.amount.toString()
: "",
minValue: channelData.minimumOrderPrice
? channelData.minimumOrderPrice.amount.toString()
: "",
name: channelData.channel.name,
price: channelData.price ? channelData.price.amount.toString() : ""
})) || [];
export const createCollectionChannelsData = (
collectionData?: CollectionDetails_collection
) => {
if (collectionData?.channelListings) {
const collectionDataArr = collectionData?.channelListings.map(listing => ({
id: listing.channel.id,
isPublished: listing.isPublished,
name: listing.channel.name,
publicationDate: listing.publicationDate
}));
return collectionDataArr;
}
};
export interface ChannelShippingData {
currency: string;
id: string;
minValue: string;
name: string;
maxValue: string;
price: string;
}
export const createChannelsDataFromVoucher = (
voucherData?: VoucherDetails_voucher
) =>
voucherData?.channelListings?.map(option => ({
currency: option.channel.currencyCode || option?.minSpent?.currency || "",
discountValue: option.discountValue.toString() || "",
id: option.channel.id,
minSpent: option?.minSpent?.amount.toString() || "",
name: option.channel.name
})) || [];
export const createChannelsDataFromSale = (saleData?: SaleDetails_sale) =>
saleData?.channelListings?.map(option => ({
currency: option.channel.currencyCode || "",
discountValue: option.discountValue.toString() || "",
id: option.channel.id,
name: option.channel.name
})) || [];
export const createChannelsDataFromProduct = (
productData?: ProductDetails_product
) =>
productData?.channelListings?.map(option => {
const variantChannel = productData.variants[0]?.channelListings.find(
listing => listing.channel.id === option.channel.id
);
const price = variantChannel?.price;
const costPrice = variantChannel?.costPrice;
return {
availableForPurchase: option?.availableForPurchase,
costPrice: costPrice ? costPrice.amount.toString() : "",
currency: price ? price.currency : "",
id: option.channel.id,
isAvailableForPurchase: !!option?.isAvailableForPurchase,
isPublished: option.isPublished,
name: option.channel.name,
price: price ? price.amount.toString() : "",
publicationDate: option.publicationDate,
visibleInListings: !!option.visibleInListings
};
}) || [];
export const createSortedChannelsDataFromProduct = (
productData?: ProductDetails_product
) =>
createChannelsDataFromProduct(productData).sort((channel, nextChannel) =>
channel.name.localeCompare(nextChannel.name)
);
export const createSortedChannelsData = (data?: Channels_channels[]) =>
createChannelsData(data)?.sort((channel, nextChannel) =>
channel.name.localeCompare(nextChannel.name)
);
export const createSortedShippingChannels = (data?: Channels_channels[]) =>
createShippingChannels(data)?.sort((channel, nextChannel) =>
channel.name.localeCompare(nextChannel.name)
);
export const createSortedShippingChannelsFromRate = (
data?: ShippingZone_shippingZone_shippingMethods_channelListings[]
) =>
createShippingChannelsFromRate(data)?.sort((channel, nextChannel) =>
channel.name.localeCompare(nextChannel.name)
);
export const createSortedVoucherData = (data?: Channels_channels[]) =>
createVoucherChannels(data)?.sort((channel, nextChannel) =>
channel.name.localeCompare(nextChannel.name)
);
export const createSortedSaleData = (data?: Channels_channels[]) =>
createSaleChannels(data)?.sort((channel, nextChannel) =>
channel.name.localeCompare(nextChannel.name)
);
export const createSortedChannelsDataFromVoucher = (
data?: VoucherDetails_voucher
) =>
createChannelsDataFromVoucher(data)?.sort((channel, nextChannel) =>
channel.name.localeCompare(nextChannel.name)
);
export const createSortedChannelsDataFromSale = (data?: SaleDetails_sale) =>
createChannelsDataFromSale(data)?.sort((channel, nextChannel) =>
channel.name.localeCompare(nextChannel.name)
);

View file

@ -0,0 +1,92 @@
import AppHeader from "@saleor/components/AppHeader";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import { sectionNames } from "@saleor/intl";
import currencyCodes from "currency-codes";
import React from "react";
import { useIntl } from "react-intl";
import { ChannelCreateInput } from "../../../types/globalTypes";
import { useChannelCreateMutation } from "../../mutations";
import ChannelDetailsPage from "../../pages/ChannelDetailsPage";
import { ChannelCreate } from "../../types/ChannelCreate";
import { channelPath, channelsListUrl } from "../../urls";
export const ChannelCreateView = ({}) => {
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
const handleBack = () => navigate(channelsListUrl());
const onSubmit = (data: ChannelCreate) => {
if (!data.channelCreate.errors.length) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
navigate(channelPath(data.channelCreate.channel.id));
}
};
const [createChannel, createChannelOpts] = useChannelCreateMutation({
onCompleted: onSubmit
});
const handleSubmit = (data: ChannelCreateInput) =>
createChannel({
variables: {
input: { ...data, currencyCode: data.currencyCode.toUpperCase() }
}
});
const currencyCodeChoices = currencyCodes.data.map(currencyData => ({
label: intl.formatMessage(
{
defaultMessage: "{code} - {countries}",
description: "currency code select"
},
{
code: currencyData.code,
countries: currencyData.countries.join(",")
}
),
value: currencyData.code
}));
return (
<>
<WindowTitle
title={intl.formatMessage({
defaultMessage: "Create Channel",
description: "window title"
})}
/>
<Container>
<AppHeader onBack={handleBack}>
{intl.formatMessage(sectionNames.channels)}
</AppHeader>
<PageHeader
title={intl.formatMessage({
defaultMessage: "New Channel",
description: "channel create"
})}
/>
<ChannelDetailsPage
disabled={createChannelOpts.loading}
errors={createChannelOpts?.data?.channelCreate?.errors || []}
currencyCodes={currencyCodeChoices}
onSubmit={handleSubmit}
onBack={handleBack}
saveButtonBarState={createChannelOpts.status}
/>
</Container>
</>
);
};
export default ChannelCreateView;

View file

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

View file

@ -0,0 +1,125 @@
import AppHeader from "@saleor/components/AppHeader";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { ChannelErrorFragment } from "@saleor/fragments/types/ChannelErrorFragment";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import { sectionNames } from "@saleor/intl";
import getChannelsErrorMessage from "@saleor/utils/errors/channels";
import React from "react";
import { useIntl } from "react-intl";
import { ChannelUpdateInput } from "../../../types/globalTypes";
import {
useChannelActivateMutation,
useChannelDeactivateMutation,
useChannelUpdateMutation
} from "../../mutations";
import ChannelDetailsPage from "../../pages/ChannelDetailsPage";
import { useChannelDetails } from "../../queries";
import { ChannelUpdate } from "../../types/ChannelUpdate";
import { channelsListUrl } from "../../urls";
interface ChannelDetailsProps {
id: string;
}
export const ChannelDetails: React.FC<ChannelDetailsProps> = ({ id }) => {
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
const handleBack = () => navigate(channelsListUrl());
const onSubmit = (data: ChannelUpdate) => {
if (!data.channelUpdate.errors.length) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
handleBack();
}
};
const handleError = (error: ChannelErrorFragment) => {
notify({
status: "error",
text: getChannelsErrorMessage(error, intl)
});
};
const { data, loading } = useChannelDetails({
displayLoader: true,
variables: { id }
});
const [updateChannel, updateChannelOpts] = useChannelUpdateMutation({
onCompleted: onSubmit
});
const [activateChannel, activateChannelOpts] = useChannelActivateMutation({
onCompleted: data => {
const errors = data.channelActivate.errors;
if (errors.length) {
errors.forEach(error => handleError(error));
}
}
});
const [
deactivateChannel,
deactivateChannelOpts
] = useChannelDeactivateMutation({
onCompleted: data => {
const errors = data.channelDeactivate.errors;
if (errors.length) {
errors.forEach(error => handleError(error));
}
}
});
const handleSubmit = (data: ChannelUpdateInput) =>
updateChannel({
variables: {
id,
input: { name: data.name, slug: data.slug }
}
});
return (
<>
<WindowTitle
title={intl.formatMessage({
defaultMessage: "Channel details",
description: "window title"
})}
/>
<Container>
<AppHeader onBack={handleBack}>
{intl.formatMessage(sectionNames.channels)}
</AppHeader>
<PageHeader title={data?.channel?.name} />
<ChannelDetailsPage
channel={data?.channel}
disabled={updateChannelOpts.loading || loading}
disabledStatus={
activateChannelOpts.loading || deactivateChannelOpts.loading
}
errors={updateChannelOpts?.data?.channelUpdate?.errors || []}
onSubmit={handleSubmit}
onBack={handleBack}
updateChannelStatus={() =>
data?.channel?.isActive
? deactivateChannel({ variables: { id } })
: activateChannel({ variables: { id } })
}
saveButtonBarState={updateChannelOpts.status}
/>
</Container>
</>
);
};
export default ChannelDetails;

View file

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

View file

@ -0,0 +1,116 @@
import { configurationMenuUrl } from "@saleor/configuration";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import getChannelsErrorMessage from "@saleor/utils/errors/channels";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import React from "react";
import { useIntl } from "react-intl";
import ChannelDeleteDialog from "../../components/ChannelDeleteDialog";
import { useChannelDeleteMutation } from "../../mutations";
import ChannelsListPage from "../../pages/ChannelsListPage";
import { useChannelsList } from "../../queries";
import { ChannelDelete } from "../../types/ChannelDelete";
import {
channelAddUrl,
channelsListUrl,
ChannelsListUrlDialog,
ChannelsListUrlQueryParams,
channelUrl
} from "../../urls";
interface ChannelsListProps {
params: ChannelsListUrlQueryParams;
}
export const ChannelsList: React.FC<ChannelsListProps> = ({ params }) => {
const navigate = useNavigator();
const notify = useNotifier();
const intl = useIntl();
const { data, refetch } = useChannelsList({ displayLoader: true });
const selectedChannel = data?.channels.find(
channel => channel.id === params?.id
);
const [openModal, closeModal] = createDialogActionHandlers<
ChannelsListUrlDialog,
ChannelsListUrlQueryParams
>(navigate, channelsListUrl, params);
const onCompleted = (data: ChannelDelete) => {
const errors = data.channelDelete.errors;
if (errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Channel deleted"
})
});
refetch();
closeModal();
} else {
errors.map(error =>
notify({
status: "error",
text: getChannelsErrorMessage(error, intl)
})
);
}
};
const [deleteChannel, deleteChannelOpts] = useChannelDeleteMutation({
onCompleted
});
const channelsChoices = params.id
? data?.channels
?.filter(
channel =>
channel.id !== params.id &&
channel.currencyCode === selectedChannel.currencyCode
)
.map(channel => ({
label: channel.name,
value: channel.id
}))
: [];
const navigateToChannelCreate = () => navigate(channelAddUrl);
const handleRemoveConfirm = (id: string) =>
deleteChannel({
variables: { id: params.id, input: { targetChannel: id } }
});
return (
<>
<ChannelsListPage
channelsList={data?.channels}
navigateToChannelCreate={navigateToChannelCreate}
onBack={() => navigate(configurationMenuUrl)}
onRowClick={id => () => navigate(channelUrl(id))}
onRemove={id =>
openModal("remove", {
id
})
}
/>
{!!selectedChannel && (
<ChannelDeleteDialog
channelsChoices={channelsChoices}
open={params.action === "remove"}
confirmButtonState={deleteChannelOpts.status}
onBack={() => navigate(channelsListUrl())}
onClose={closeModal}
onConfirm={handleRemoveConfirm}
/>
)}
</>
);
};
ChannelsList.displayName = "ChannelsList";
export default ChannelsList;

View file

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

View file

@ -1,4 +1,6 @@
import { ChannelCollectionData } from "@saleor/channels/utils";
import AppHeader from "@saleor/components/AppHeader";
import { AvailabilityCard } from "@saleor/components/AvailabilityCard";
import { CardSpacer } from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import { Container } from "@saleor/components/Container";
@ -7,9 +9,8 @@ import Metadata from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm";
import VisibilityCard from "@saleor/components/VisibilityCard";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import useDateLocalize from "@saleor/hooks/useDateLocalize";
import { CollectionChannelListingErrorFragment } from "@saleor/fragments/types/CollectionChannelListingErrorFragment";
import { CollectionErrorFragment } from "@saleor/fragments/types/CollectionErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl";
import React from "react";
@ -20,25 +21,38 @@ import { CollectionImage } from "../CollectionImage/CollectionImage";
import CollectionCreateForm, { CollectionCreateData } from "./form";
export interface CollectionCreatePageProps {
channelsCount: number;
channelsErrors: CollectionChannelListingErrorFragment[];
currentChannels: ChannelCollectionData[];
disabled: boolean;
errors: ProductErrorFragment[];
errors: CollectionErrorFragment[];
saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void;
onSubmit: (data: CollectionCreateData) => SubmitPromise;
onChannelsChange: (data: ChannelCollectionData[]) => void;
openChannelsModal: () => void;
}
const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
channelsCount,
channelsErrors,
currentChannels = [],
disabled,
errors,
saveButtonBarState,
onBack,
onChannelsChange,
openChannelsModal,
onSubmit
}: CollectionCreatePageProps) => {
const intl = useIntl();
const localizeDate = useDateLocalize();
return (
<CollectionCreateForm onSubmit={onSubmit}>
<CollectionCreateForm
onSubmit={onSubmit}
currentChannels={currentChannels}
setChannels={onChannelsChange}
>
{({ change, data, handlers, hasChanged, submit }) => (
<Container>
<AppHeader onBack={onBack}>
@ -115,30 +129,25 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
<Metadata data={data} onChange={handlers.changeMetadata} />
</div>
<div>
<VisibilityCard
data={data}
errors={errors}
disabled={disabled}
<AvailabilityCard
messages={{
hiddenLabel: intl.formatMessage({
defaultMessage: "Hidden",
description: "collection label"
}),
hiddenSecondLabel: intl.formatMessage(
{
defaultMessage: "will be visible from {date}",
description: "collection"
},
{
date: localizeDate(data.publicationDate, "L")
}
),
visibleLabel: intl.formatMessage({
defaultMessage: "Visible",
description: "collection label"
})
}}
onChange={change}
errors={channelsErrors}
selectedChannelsCount={data.channelListings.length}
allChannelsCount={channelsCount}
channels={data.channelListings}
disabled={disabled}
onChange={handlers.changeChannels}
openModal={openChannelsModal}
/>
</div>
</Grid>

View file

@ -1,4 +1,6 @@
import { OutputData } from "@editorjs/editorjs";
import { ChannelCollectionData } from "@saleor/channels/utils";
import { createChannelsChangeHandler } from "@saleor/collections/utils";
import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm";
@ -13,10 +15,9 @@ export interface CollectionCreateFormData extends MetadataFormData {
value: string;
};
backgroundImageAlt: string;
channelListings: ChannelCollectionData[];
name: string;
slug: string;
publicationDate: string;
isPublished: boolean;
seoDescription: string;
seoTitle: string;
}
@ -27,6 +28,10 @@ export interface CollectionCreateData extends CollectionCreateFormData {
interface CollectionCreateHandlers {
changeMetadata: FormChange;
changeDescription: RichTextEditorChange;
changeChannels: (
id: string,
data: Omit<ChannelCollectionData, "name" | "id">
) => void;
}
export interface UseCollectionCreateFormResult {
change: FormChange;
@ -37,11 +42,15 @@ export interface UseCollectionCreateFormResult {
}
export interface CollectionCreateFormProps {
currentChannels: ChannelCollectionData[];
setChannels: (data: ChannelCollectionData[]) => void;
children: (props: UseCollectionCreateFormResult) => React.ReactNode;
onSubmit: (data: CollectionCreateData) => SubmitPromise;
}
function useCollectionCreateForm(
currentChannels: ChannelCollectionData[],
setChannels: (data: ChannelCollectionData[]) => void,
onSubmit: (data: CollectionCreateData) => SubmitPromise
): UseCollectionCreateFormResult {
const [changed, setChanged] = React.useState(false);
@ -53,11 +62,10 @@ function useCollectionCreateForm(
value: null
},
backgroundImageAlt: "",
isPublished: false,
channelListings: currentChannels,
metadata: [],
name: "",
privateMetadata: [],
publicationDate: "",
seoDescription: "",
seoTitle: "",
slug: ""
@ -83,12 +91,19 @@ function useCollectionCreateForm(
description: description.current
});
const handleChannelChange = createChannelsChangeHandler(
currentChannels,
setChannels,
triggerChange
);
const submit = () => handleFormSubmit(getData(), onSubmit, setChanged);
return {
change: handleChange,
data: getData(),
handlers: {
changeChannels: handleChannelChange,
changeDescription,
changeMetadata
},
@ -98,10 +113,12 @@ function useCollectionCreateForm(
}
const CollectionCreateForm: React.FC<CollectionCreateFormProps> = ({
currentChannels,
setChannels,
children,
onSubmit
}) => {
const props = useCollectionCreateForm(onSubmit);
const props = useCollectionCreateForm(currentChannels, setChannels, onSubmit);
return <form onSubmit={props.submit}>{children(props)}</form>;
};

View file

@ -7,7 +7,7 @@ import FormSpacer from "@saleor/components/FormSpacer";
import RichTextEditor, {
RichTextEditorChange
} from "@saleor/components/RichTextEditor";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { CollectionErrorFragment } from "@saleor/fragments/types/CollectionErrorFragment";
import { commonMessages } from "@saleor/intl";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import React from "react";
@ -19,7 +19,7 @@ export interface CollectionDetailsProps {
name: string;
};
disabled: boolean;
errors: ProductErrorFragment[];
errors: CollectionErrorFragment[];
onChange: (event: React.ChangeEvent<any>) => void;
onDescriptionChange: RichTextEditorChange;
}

View file

@ -1,24 +1,21 @@
import { ChannelCollectionData } from "@saleor/channels/utils";
import AppHeader from "@saleor/components/AppHeader";
import { AvailabilityCard } from "@saleor/components/AvailabilityCard";
import { CardSpacer } from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import { Container } from "@saleor/components/Container";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid";
import Hr from "@saleor/components/Hr";
import Metadata from "@saleor/components/Metadata/Metadata";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm";
import VisibilityCard from "@saleor/components/VisibilityCard";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import useDateLocalize from "@saleor/hooks/useDateLocalize";
import { CollectionChannelListingErrorFragment } from "@saleor/fragments/types/CollectionChannelListingErrorFragment";
import { CollectionErrorFragment } from "@saleor/fragments/types/CollectionErrorFragment";
import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl";
import React from "react";
import { useIntl } from "react-intl";
import { maybe } from "../../../misc";
import { ListActions, PageListProps } from "../../../types";
import { CollectionDetails_collection } from "../../types/CollectionDetails";
import CollectionDetails from "../CollectionDetails/CollectionDetails";
@ -27,38 +24,50 @@ import CollectionProducts from "../CollectionProducts/CollectionProducts";
import CollectionUpdateForm, { CollectionUpdateData } from "./form";
export interface CollectionDetailsPageProps extends PageListProps, ListActions {
channelsCount: number;
channelsErrors: CollectionChannelListingErrorFragment[];
collection: CollectionDetails_collection;
errors: ProductErrorFragment[];
isFeatured: boolean;
currentChannels: ChannelCollectionData[];
errors: CollectionErrorFragment[];
hasChannelChanged: boolean;
saveButtonBarState: ConfirmButtonTransitionState;
selectedChannel: string;
onBack: () => void;
onCollectionRemove: () => void;
onImageDelete: () => void;
onImageUpload: (file: File) => void;
onProductUnassign: (id: string, event: React.MouseEvent<any>) => void;
onSubmit: (data: CollectionUpdateData) => SubmitPromise;
onChannelsChange: (data: ChannelCollectionData[]) => void;
openChannelsModal: () => void;
}
const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
channelsCount,
channelsErrors,
collection,
currentChannels = [],
disabled,
errors,
isFeatured,
hasChannelChanged,
saveButtonBarState,
selectedChannel,
onBack,
onCollectionRemove,
onImageDelete,
onImageUpload,
onSubmit,
onChannelsChange,
openChannelsModal,
...collectionProductsProps
}: CollectionDetailsPageProps) => {
const intl = useIntl();
const localizeDate = useDateLocalize();
return (
<CollectionUpdateForm
collection={collection}
isFeatured={isFeatured}
currentChannels={currentChannels}
setChannels={onChannelsChange}
onSubmit={onSubmit}
>
{({ change, data, handlers, hasChanged, submit }) => (
@ -66,7 +75,7 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.collections)}
</AppHeader>
<PageHeader title={maybe(() => collection.name)} />
<PageHeader title={collection?.name} />
<Grid>
<div>
<CollectionDetails
@ -79,7 +88,7 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
<CardSpacer />
<CollectionImage
data={data}
image={maybe(() => collection.backgroundImage)}
image={collection?.backgroundImage}
onImageDelete={onImageDelete}
onImageUpload={onImageUpload}
onChange={change}
@ -89,6 +98,8 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
<CardSpacer />
<CollectionProducts
disabled={disabled}
channelsCount={channelsCount}
selectedChannel={selectedChannel}
collection={collection}
{...collectionProductsProps}
/>
@ -105,55 +116,38 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
slug={data.slug}
slugPlaceholder={data.name}
title={data.seoTitle}
titlePlaceholder={maybe(() => collection.name)}
titlePlaceholder={collection?.name}
onChange={change}
/>
</div>
<div>
<div>
<VisibilityCard
data={data}
errors={errors}
<AvailabilityCard
messages={{
hiddenLabel: intl.formatMessage({
defaultMessage: "Hidden",
description: "collection label"
}),
hiddenSecondLabel: intl.formatMessage(
{
defaultMessage: "will be visible from {date}",
description: "collection"
},
{
date: localizeDate(data.publicationDate, "L")
}
),
visibleLabel: intl.formatMessage({
defaultMessage: "Visible",
description: "collection label"
})
}}
onChange={change}
>
<FormSpacer />
<Hr />
<ControlledCheckbox
name={"isFeatured" as keyof CollectionUpdateData}
label={intl.formatMessage({
defaultMessage: "Feature on Homepage",
description: "switch button"
})}
checked={data.isFeatured}
onChange={change}
disabled={disabled}
/>
</VisibilityCard>
errors={channelsErrors}
selectedChannelsCount={data.channelListings.length}
allChannelsCount={channelsCount}
channels={data.channelListings}
disabled={disabled}
onChange={handlers.changeChannels}
openModal={openChannelsModal}
/>
</div>
</div>
</Grid>
<SaveButtonBar
state={saveButtonBarState}
disabled={disabled || !hasChanged}
disabled={disabled || (!hasChanged && !hasChannelChanged)}
onCancel={onBack}
onDelete={onCollectionRemove}
onSave={submit}

View file

@ -1,9 +1,10 @@
import { OutputData } from "@editorjs/editorjs";
import { ChannelCollectionData } from "@saleor/channels/utils";
import { CollectionDetails_collection } from "@saleor/collections/types/CollectionDetails";
import { createChannelsChangeHandler } from "@saleor/collections/utils";
import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, { FormChange } from "@saleor/hooks/useForm";
import getPublicationData from "@saleor/utils/data/getPublicationData";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata";
@ -13,13 +14,11 @@ import React from "react";
export interface CollectionUpdateFormData extends MetadataFormData {
backgroundImageAlt: string;
channelListings: ChannelCollectionData[];
name: string;
slug: string;
publicationDate: string;
seoDescription: string;
seoTitle: string;
isFeatured: boolean;
isPublished: boolean;
}
export interface CollectionUpdateData extends CollectionUpdateFormData {
description: OutputData;
@ -28,6 +27,10 @@ export interface CollectionUpdateData extends CollectionUpdateFormData {
interface CollectionUpdateHandlers {
changeMetadata: FormChange;
changeDescription: RichTextEditorChange;
changeChannels: (
id: string,
data: Omit<ChannelCollectionData, "name" | "id">
) => void;
}
export interface UseCollectionUpdateFormResult {
change: FormChange;
@ -40,26 +43,26 @@ export interface UseCollectionUpdateFormResult {
export interface CollectionUpdateFormProps {
children: (props: UseCollectionUpdateFormResult) => React.ReactNode;
collection: CollectionDetails_collection;
isFeatured: boolean;
currentChannels: ChannelCollectionData[];
setChannels: (data: ChannelCollectionData[]) => void;
onSubmit: (data: CollectionUpdateData) => Promise<any[]>;
}
function useCollectionUpdateForm(
collection: CollectionDetails_collection,
onSubmit: (data: CollectionUpdateData) => Promise<any[]>,
isFeatured: boolean
currentChannels: ChannelCollectionData[],
setChannels: (data: ChannelCollectionData[]) => void,
onSubmit: (data: CollectionUpdateData) => Promise<any[]>
): UseCollectionUpdateFormResult {
const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true);
const form = useForm<CollectionUpdateFormData>({
backgroundImageAlt: collection?.backgroundImage?.alt || "",
isFeatured,
isPublished: !!collection?.isPublished,
channelListings: currentChannels,
metadata: collection?.metadata?.map(mapMetadataItemToInput),
name: collection?.name || "",
privateMetadata: collection?.privateMetadata?.map(mapMetadataItemToInput),
publicationDate: collection?.publicationDate || "",
seoDescription: collection?.seoDescription || "",
seoTitle: collection?.seoTitle || "",
slug: collection?.slug || ""
@ -89,16 +92,22 @@ function useCollectionUpdateForm(
const getSubmitData = (): CollectionUpdateData => ({
...getData(),
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified),
...getPublicationData(form.data)
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified)
});
const handleChannelChange = createChannelsChangeHandler(
currentChannels,
setChannels,
triggerChange
);
const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged);
return {
change: handleChange,
data: getData(),
handlers: {
changeChannels: handleChannelChange,
changeDescription,
changeMetadata
},
@ -108,12 +117,18 @@ function useCollectionUpdateForm(
}
const CollectionUpdateForm: React.FC<CollectionUpdateFormProps> = ({
children,
collection,
isFeatured,
currentChannels,
setChannels,
children,
onSubmit
}) => {
const props = useCollectionUpdateForm(collection, onSubmit, isFeatured);
const props = useCollectionUpdateForm(
collection,
currentChannels,
setChannels,
onSubmit
);
return <form onSubmit={props.submit}>{children(props)}</form>;
};

View file

@ -4,10 +4,10 @@ import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import { CollectionListUrlSortField } from "@saleor/collections/urls";
import { ChannelsAvailabilityDropdown } from "@saleor/components/ChannelsAvailabilityDropdown";
import Checkbox from "@saleor/components/Checkbox";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableCellHeader from "@saleor/components/TableCellHeader";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
@ -15,7 +15,7 @@ import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps, SortPage } from "@saleor/types";
import { getArrowDirection } from "@saleor/utils/sort";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { FormattedMessage } from "react-intl";
import { CollectionList_collections_edges_node } from "../../types/CollectionList";
@ -49,12 +49,15 @@ interface CollectionListProps
ListActions,
SortPage<CollectionListUrlSortField> {
collections: CollectionList_collections_edges_node[];
channelsCount: number;
selectedChannel: string;
}
const numberOfColumns = 4;
const CollectionList: React.FC<CollectionListProps> = props => {
const {
channelsCount,
collections,
disabled,
settings,
@ -67,13 +70,13 @@ const CollectionList: React.FC<CollectionListProps> = props => {
pageInfo,
isChecked,
selected,
selectedChannel,
toggle,
toggleAll,
toolbar
} = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<ResponsiveTable>
@ -143,6 +146,9 @@ const CollectionList: React.FC<CollectionListProps> = props => {
collections,
collection => {
const isSelected = collection ? isChecked(collection.id) : false;
const channel = collection?.channelListings.find(
listing => listing.channel.id === selectedChannel
);
return (
<TableRow
className={classes.tableRow}
@ -172,26 +178,18 @@ const CollectionList: React.FC<CollectionListProps> = props => {
</TableCell>
<TableCell
className={classes.colAvailability}
data-test="published"
data-test-published={maybe(() => collection.isPublished)}
data-test="availability"
data-test-availability={!!collection?.channelListings?.length}
>
{maybe(
() => (
<StatusLabel
status={collection.isPublished ? "success" : "error"}
label={
collection.isPublished
? intl.formatMessage({
defaultMessage: "Published",
description: "collection is published"
})
: intl.formatMessage({
defaultMessage: "Not published",
description: "collection is not published"
})
}
/>
),
{collection && !collection?.channelListings?.length ? (
"-"
) : collection?.channelListings !== undefined ? (
<ChannelsAvailabilityDropdown
allChannelsCount={channelsCount}
currentChannel={channel}
channels={collection?.channelListings}
/>
) : (
<Skeleton />
)}
</TableCell>

View file

@ -1,14 +1,16 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import makeStyles from "@material-ui/core/styles/makeStyles";
import { CollectionListUrlSortField } from "@saleor/collections/urls";
import CardMenu from "@saleor/components/CardMenu";
import { Container } from "@saleor/components/Container";
import FilterBar from "@saleor/components/FilterBar";
import PageHeader from "@saleor/components/PageHeader";
import SearchBar from "@saleor/components/SearchBar";
import { sectionNames } from "@saleor/intl";
import {
FilterPageProps,
ListActions,
PageListProps,
SearchPageProps,
SortPage,
TabPageProps
} from "@saleor/types";
@ -17,44 +19,64 @@ import { FormattedMessage, useIntl } from "react-intl";
import { CollectionList_collections_edges_node } from "../../types/CollectionList";
import CollectionList from "../CollectionList/CollectionList";
import {
CollectionFilterKeys,
CollectionListFilterOpts,
createFilterStructure
} from "./filters";
export interface CollectionListPageProps
extends PageListProps,
ListActions,
FilterPageProps<CollectionFilterKeys, CollectionListFilterOpts>,
SearchPageProps,
SortPage<CollectionListUrlSortField>,
TabPageProps {
collections: CollectionList_collections_edges_node[];
channelsCount: number;
selectedChannel: string;
onSettingsOpen?: () => void;
}
const useStyles = makeStyles(
theme => ({
settings: {
marginRight: theme.spacing(2)
}
}),
{ name: "CollectionListPage" }
);
const CollectionListPage: React.FC<CollectionListPageProps> = ({
currencySymbol,
channelsCount,
currentTab,
disabled,
filterOpts,
initialSearch,
onAdd,
onAll,
onFilterChange,
onSearchChange,
onSettingsOpen,
onTabChange,
onTabDelete,
onTabSave,
selectedChannel,
tabs,
...listProps
}) => {
const intl = useIntl();
const structure = createFilterStructure(intl, filterOpts);
const classes = useStyles({});
return (
<Container>
<PageHeader title={intl.formatMessage(sectionNames.collections)}>
{!!onSettingsOpen && (
<CardMenu
className={classes.settings}
menuItems={[
{
label: intl.formatMessage({
defaultMessage: "Settings",
description: "button"
}),
onSelect: onSettingsOpen
}
]}
/>
)}
<Button
color="primary"
disabled={disabled}
@ -68,27 +90,29 @@ const CollectionListPage: React.FC<CollectionListPageProps> = ({
</Button>
</PageHeader>
<Card>
<FilterBar
<SearchBar
allTabLabel={intl.formatMessage({
defaultMessage: "All Collections",
description: "tab name"
})}
currencySymbol={currencySymbol}
currentTab={currentTab}
filterStructure={structure}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
defaultMessage: "Search Collection"
})}
tabs={tabs}
onAll={onAll}
onFilterChange={onFilterChange}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
/>
<CollectionList disabled={disabled} {...listProps} />
<CollectionList
disabled={disabled}
channelsCount={channelsCount}
selectedChannel={selectedChannel}
{...listProps}
/>
</Card>
</Container>
);

View file

@ -8,10 +8,10 @@ import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import DeleteIcon from "@material-ui/icons/Delete";
import CardTitle from "@saleor/components/CardTitle";
import { ChannelsAvailabilityDropdown } from "@saleor/components/ChannelsAvailabilityDropdown";
import Checkbox from "@saleor/components/Checkbox";
import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
@ -57,6 +57,8 @@ const useStyles = makeStyles(
export interface CollectionProductsProps extends PageListProps, ListActions {
collection: CollectionDetails_collection;
channelsCount: number;
selectedChannel: string;
onProductUnassign: (id: string, event: React.MouseEvent<any>) => void;
}
@ -64,6 +66,7 @@ const numberOfColumns = 5;
const CollectionProducts: React.FC<CollectionProductsProps> = props => {
const {
channelsCount,
collection,
disabled,
onAdd,
@ -72,6 +75,7 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
onProductUnassign,
onRowClick,
pageInfo,
selectedChannel,
isChecked,
selected,
toggle,
@ -139,8 +143,8 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
</TableCell>
<TableCell className={classes.colPublished}>
<FormattedMessage
defaultMessage="Published"
description="product is published"
defaultMessage="Availability"
description="product availability"
/>
</TableCell>
<TableCell className={classes.colActions} />
@ -149,9 +153,9 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
<TableRow>
<TablePagination
colSpan={numberOfColumns}
hasNextPage={maybe(() => pageInfo.hasNextPage)}
hasNextPage={pageInfo?.hasNextPage}
onNextPage={onNextPage}
hasPreviousPage={maybe(() => pageInfo.hasPreviousPage)}
hasPreviousPage={pageInfo?.hasPreviousPage}
onPreviousPage={onPreviousPage}
/>
</TableRow>
@ -161,6 +165,10 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
maybe(() => collection.products.edges.map(edge => edge.node)),
product => {
const isSelected = product ? isChecked(product.id) : false;
const channel =
product?.channelListings.find(
listing => listing.channel.id === selectedChannel
) || product?.channelListings[0];
return (
<TableRow
@ -190,24 +198,16 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colPublished}>
{maybe(
() => (
<StatusLabel
label={
product.isPublished
? intl.formatMessage({
defaultMessage: "Published",
description: "product is published"
})
: intl.formatMessage({
defaultMessage: "Not published",
description: "product is not published"
})
}
status={product.isPublished ? "success" : "error"}
/>
),
<TableCell className={classes.colType}>
{product && !product?.channelListings?.length ? (
"-"
) : product?.channelListings !== undefined ? (
<ChannelsAvailabilityDropdown
allChannelsCount={channelsCount}
currentChannel={channel}
channels={product?.channelListings}
/>
) : (
<Skeleton />
)}
</TableCell>

View file

@ -7,8 +7,19 @@ const content = richTextEditorFixtures.richTextEditor;
export const collections: CollectionList_collections_edges_node[] = [
{
__typename: "Collection",
channelListings: [
{
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "123",
name: "Channel"
},
isPublished: false,
publicationDate: null
}
],
id: "Q29sbGVjdGlvbjox",
isPublished: true,
name: "Summer collection",
products: {
__typename: "ProductCountableConnection",
@ -17,8 +28,19 @@ export const collections: CollectionList_collections_edges_node[] = [
},
{
__typename: "Collection",
channelListings: [
{
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "124",
name: "Channel"
},
isPublished: false,
publicationDate: null
}
],
id: "Q29sbGVjdGlvbjoy",
isPublished: true,
name: "Winter sale",
products: {
__typename: "ProductCountableConnection",
@ -27,8 +49,19 @@ export const collections: CollectionList_collections_edges_node[] = [
},
{
__typename: "Collection",
channelListings: [
{
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "125",
name: "Channel"
},
isPublished: false,
publicationDate: null
}
],
id: "Q29sbGVjdGlvbjoz",
isPublished: true,
name: "Vintage vibes",
products: {
__typename: "ProductCountableConnection",
@ -37,8 +70,19 @@ export const collections: CollectionList_collections_edges_node[] = [
},
{
__typename: "Collection",
channelListings: [
{
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "126",
name: "Channel"
},
isPublished: false,
publicationDate: null
}
],
id: "Q29sbGVjdGlvbjoa",
isPublished: true,
name: "Merry Christmas",
products: {
__typename: "ProductCountableConnection",
@ -47,8 +91,19 @@ export const collections: CollectionList_collections_edges_node[] = [
},
{
__typename: "Collection",
channelListings: [
{
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "127",
name: "Channel"
},
isPublished: false,
publicationDate: null
}
],
id: "Q29sbGVjdGlvbjob",
isPublished: true,
name: "80s Miami",
products: {
__typename: "ProductCountableConnection",
@ -57,8 +112,19 @@ export const collections: CollectionList_collections_edges_node[] = [
},
{
__typename: "Collection",
channelListings: [
{
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "128",
name: "Channel"
},
isPublished: false,
publicationDate: null
}
],
id: "Q29sbGVjdGlvbjoc",
isPublished: true,
name: "Yellow Submarine 2019",
products: {
__typename: "ProductCountableConnection",
@ -79,9 +145,20 @@ export const collection: (
alt: "Alt text",
url: placeholderCollectionImage
},
channelListings: [
{
__typename: "CollectionChannelListing",
channel: {
__typename: "Channel",
id: "223",
name: "Channel"
},
isPublished: false,
publicationDate: null
}
],
descriptionJson: JSON.stringify(content),
id: "Q29sbGVjdGlvbjox",
isPublished: true,
metadata: [
{
__typename: "MetadataItem",
@ -96,11 +173,49 @@ export const collection: (
edges: [
{
__typename: "ProductCountableEdge",
cursor: "YXJyYXljb25uZWN0aW9uOjA=",
node: {
__typename: "Product",
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "123",
name: "Channel1"
},
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: true,
publicationDate: "2020-07-14",
visibleInListings: true
},
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "124",
name: "Channel2"
},
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: "2020-07-30",
visibleInListings: true
}
],
id: "UHJvZHVjdDoxNw==",
isPublished: true,
name: "Murray Inc",
productType: {
__typename: "ProductType",
@ -112,11 +227,49 @@ export const collection: (
},
{
__typename: "ProductCountableEdge",
cursor: "YXJyYXljb25uZWN0aW9uOjE=",
node: {
__typename: "Product",
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "123",
name: "Channel1"
},
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: true,
publicationDate: "2020-07-14",
visibleInListings: true
},
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "124",
name: "Channel2"
},
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: "2020-07-30",
visibleInListings: true
}
],
id: "UHJvZHVjdDoyNw==",
isPublished: true,
name: "Williams-Taylor",
productType: {
__typename: "ProductType",
@ -128,11 +281,49 @@ export const collection: (
},
{
__typename: "ProductCountableEdge",
cursor: "YXJyYXljb25uZWN0aW9uOjI=",
node: {
__typename: "Product",
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "123",
name: "Channel1"
},
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: true,
publicationDate: "2020-07-14",
visibleInListings: false
},
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "124",
name: "Channel2"
},
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: "2020-07-30",
visibleInListings: false
}
],
id: "UHJvZHVjdDoyOQ==",
isPublished: true,
name: "Hebert-Sherman",
productType: {
__typename: "ProductType",
@ -144,11 +335,49 @@ export const collection: (
},
{
__typename: "ProductCountableEdge",
cursor: "YXJyYXljb25uZWN0aW9uOjM=",
node: {
__typename: "Product",
channelListings: [
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "123",
name: "Channel1"
},
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: true,
publicationDate: "2020-07-14",
visibleInListings: false
},
{
__typename: "ProductChannelListing",
availableForPurchase: null,
channel: {
__typename: "Channel",
currencyCode: "USD",
id: "124",
name: "Channel2"
},
discountedPrice: {
__typename: "Money",
amount: 1,
currency: "USD"
},
isAvailableForPurchase: false,
isPublished: false,
publicationDate: "2020-07-30",
visibleInListings: false
}
],
id: "UHJvZHVjdDo1Mw==",
isPublished: true,
name: "Estes, Johnson and Graham",
productType: {
__typename: "ProductType",
@ -167,7 +396,6 @@ export const collection: (
startCursor: ""
}
},
publicationDate: "2018-08-25T18:45:54.125Z",
seoDescription: "",
seoTitle: "",
slug: "summer-collection"

View file

@ -1,10 +1,15 @@
import {
CollectionChannelListingUpdate,
CollectionChannelListingUpdateVariables
} from "@saleor/collections/types/CollectionChannelListingUpdate";
import {
collectionDetailsFragment,
collectionProductFragment
} from "@saleor/fragments/collections";
import {
productErrorFragment,
shopErrorFragment
collectionChannelListingErrorFragment,
collectionsErrorFragment,
productErrorFragment
} from "@saleor/fragments/errors";
import makeMutation from "@saleor/hooks/makeMutation";
import gql from "graphql-tag";
@ -17,18 +22,10 @@ import {
CollectionBulkDelete,
CollectionBulkDeleteVariables
} from "./types/CollectionBulkDelete";
import {
CollectionBulkPublish,
CollectionBulkPublishVariables
} from "./types/CollectionBulkPublish";
import {
CollectionUpdate,
CollectionUpdateVariables
} from "./types/CollectionUpdate";
import {
CollectionUpdateWithHomepage,
CollectionUpdateWithHomepageVariables
} from "./types/CollectionUpdateWithHomepage";
import {
CreateCollection,
CreateCollectionVariables
@ -44,14 +41,14 @@ import {
const collectionUpdate = gql`
${collectionDetailsFragment}
${productErrorFragment}
${collectionsErrorFragment}
mutation CollectionUpdate($id: ID!, $input: CollectionInput!) {
collectionUpdate(id: $id, input: $input) {
collection {
...CollectionDetailsFragment
}
errors: productErrors {
...ProductErrorFragment
errors: collectionErrors {
...CollectionErrorFragment
}
}
}
@ -61,43 +58,9 @@ export const useCollectionUpdateMutation = makeMutation<
CollectionUpdateVariables
>(collectionUpdate);
const collectionUpdateWithHomepage = gql`
${collectionDetailsFragment}
${productErrorFragment}
${shopErrorFragment}
mutation CollectionUpdateWithHomepage(
$id: ID!
$input: CollectionInput!
$homepageId: ID
) {
homepageCollectionUpdate(collection: $homepageId) {
errors: shopErrors {
...ShopErrorFragment
}
shop {
homepageCollection {
id
}
}
}
collectionUpdate(id: $id, input: $input) {
collection {
...CollectionDetailsFragment
}
errors: productErrors {
...ProductErrorFragment
}
}
}
`;
export const useCollectionUpdateWithHomepageMutation = makeMutation<
CollectionUpdateWithHomepage,
CollectionUpdateWithHomepageVariables
>(collectionUpdateWithHomepage);
const assignCollectionProduct = gql`
${collectionProductFragment}
${productErrorFragment}
${collectionsErrorFragment}
mutation CollectionAssignProduct(
$collectionId: ID!
$productIds: [ID!]!
@ -123,8 +86,8 @@ const assignCollectionProduct = gql`
}
}
}
errors: productErrors {
...ProductErrorFragment
errors: collectionErrors {
...CollectionErrorFragment
}
}
}
@ -136,14 +99,14 @@ export const useCollectionAssignProductMutation = makeMutation<
const createCollection = gql`
${collectionDetailsFragment}
${productErrorFragment}
${collectionsErrorFragment}
mutation CreateCollection($input: CollectionCreateInput!) {
collectionCreate(input: $input) {
collection {
...CollectionDetailsFragment
}
errors: productErrors {
...ProductErrorFragment
errors: collectionErrors {
...CollectionErrorFragment
}
}
}
@ -154,11 +117,11 @@ export const useCollectionCreateMutation = makeMutation<
>(createCollection);
const removeCollection = gql`
${productErrorFragment}
${collectionsErrorFragment}
mutation RemoveCollection($id: ID!) {
collectionDelete(id: $id) {
errors: productErrors {
...ProductErrorFragment
errors: collectionErrors {
...CollectionErrorFragment
}
}
}
@ -169,7 +132,7 @@ export const useCollectionRemoveMutation = makeMutation<
>(removeCollection);
const unassignCollectionProduct = gql`
${productErrorFragment}
${collectionsErrorFragment}
mutation UnassignCollectionProduct(
$collectionId: ID!
$productIds: [ID]!
@ -188,7 +151,6 @@ const unassignCollectionProduct = gql`
edges {
node {
id
isPublished
name
productType {
id
@ -207,8 +169,8 @@ const unassignCollectionProduct = gql`
}
}
}
errors: productErrors {
...ProductErrorFragment
errors: collectionErrors {
...CollectionErrorFragment
}
}
}
@ -233,17 +195,20 @@ export const useCollectionBulkDelete = makeMutation<
CollectionBulkDeleteVariables
>(collectionBulkDelete);
const collectionBulkPublish = gql`
${productErrorFragment}
mutation CollectionBulkPublish($ids: [ID]!, $isPublished: Boolean!) {
collectionBulkPublish(ids: $ids, isPublished: $isPublished) {
errors: productErrors {
...ProductErrorFragment
const collectionChannelListingUpdate = gql`
${collectionChannelListingErrorFragment}
mutation CollectionChannelListingUpdate(
$id: ID!
$input: CollectionChannelListingUpdateInput!
) {
collectionChannelListingUpdate(id: $id, input: $input) {
errors: collectionChannelListingErrors {
...CollectionChannelListingErrorFragment
}
}
}
`;
export const useCollectionBulkPublish = makeMutation<
CollectionBulkPublish,
CollectionBulkPublishVariables
>(collectionBulkPublish);
export const useCollectionChannelListingUpdate = makeMutation<
CollectionChannelListingUpdate,
CollectionChannelListingUpdateVariables
>(collectionChannelListingUpdate);

View file

@ -82,11 +82,6 @@ export const collectionDetails = gql`
}
}
}
shop {
homepageCollection {
id
}
}
}
`;
export const TypedCollectionDetailsQuery = TypedQuery<

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ProductErrorCode } from "./../../types/globalTypes";
import { CollectionErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: CollectionAssignProduct
@ -19,13 +19,37 @@ export interface CollectionAssignProduct_collectionAddProducts_collection_produc
url: string;
}
export interface CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_channelListings_discountedPrice {
__typename: "Money";
amount: number;
currency: string;
}
export interface CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
currencyCode: string;
}
export interface CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_channelListings {
__typename: "ProductChannelListing";
isPublished: boolean;
publicationDate: any | null;
discountedPrice: CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_channelListings_discountedPrice | null;
isAvailableForPurchase: boolean | null;
availableForPurchase: any | null;
visibleInListings: boolean;
channel: CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_channelListings_channel;
}
export interface CollectionAssignProduct_collectionAddProducts_collection_products_edges_node {
__typename: "Product";
id: string;
isPublished: boolean;
name: string;
productType: CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_productType;
thumbnail: CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_thumbnail | null;
channelListings: CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_channelListings[] | null;
}
export interface CollectionAssignProduct_collectionAddProducts_collection_products_edges {
@ -54,8 +78,8 @@ export interface CollectionAssignProduct_collectionAddProducts_collection {
}
export interface CollectionAssignProduct_collectionAddProducts_errors {
__typename: "ProductError";
code: ProductErrorCode;
__typename: "CollectionError";
code: CollectionErrorCode;
field: string | null;
}

View file

@ -0,0 +1,31 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { CollectionChannelListingUpdateInput, ProductErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: CollectionChannelListingUpdate
// ====================================================
export interface CollectionChannelListingUpdate_collectionChannelListingUpdate_errors {
__typename: "CollectionChannelListingError";
code: ProductErrorCode;
field: string | null;
message: string | null;
channels: string[] | null;
}
export interface CollectionChannelListingUpdate_collectionChannelListingUpdate {
__typename: "CollectionChannelListingUpdate";
errors: CollectionChannelListingUpdate_collectionChannelListingUpdate_errors[];
}
export interface CollectionChannelListingUpdate {
collectionChannelListingUpdate: CollectionChannelListingUpdate_collectionChannelListingUpdate | null;
}
export interface CollectionChannelListingUpdateVariables {
id: string;
input: CollectionChannelListingUpdateInput;
}

View file

@ -6,6 +6,19 @@
// GraphQL query operation: CollectionDetails
// ====================================================
export interface CollectionDetails_collection_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
}
export interface CollectionDetails_collection_channelListings {
__typename: "CollectionChannelListing";
isPublished: boolean;
publicationDate: any | null;
channel: CollectionDetails_collection_channelListings_channel;
}
export interface CollectionDetails_collection_metadata {
__typename: "MetadataItem";
key: string;
@ -35,13 +48,37 @@ export interface CollectionDetails_collection_products_edges_node_thumbnail {
url: string;
}
export interface CollectionDetails_collection_products_edges_node_channelListings_discountedPrice {
__typename: "Money";
amount: number;
currency: string;
}
export interface CollectionDetails_collection_products_edges_node_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
currencyCode: string;
}
export interface CollectionDetails_collection_products_edges_node_channelListings {
__typename: "ProductChannelListing";
isPublished: boolean;
publicationDate: any | null;
discountedPrice: CollectionDetails_collection_products_edges_node_channelListings_discountedPrice | null;
isAvailableForPurchase: boolean | null;
availableForPurchase: any | null;
visibleInListings: boolean;
channel: CollectionDetails_collection_products_edges_node_channelListings_channel;
}
export interface CollectionDetails_collection_products_edges_node {
__typename: "Product";
id: string;
isPublished: boolean;
name: string;
productType: CollectionDetails_collection_products_edges_node_productType;
thumbnail: CollectionDetails_collection_products_edges_node_thumbnail | null;
channelListings: CollectionDetails_collection_products_edges_node_channelListings[] | null;
}
export interface CollectionDetails_collection_products_edges {
@ -66,32 +103,20 @@ export interface CollectionDetails_collection_products {
export interface CollectionDetails_collection {
__typename: "Collection";
id: string;
isPublished: boolean;
name: string;
channelListings: CollectionDetails_collection_channelListings[] | null;
metadata: (CollectionDetails_collection_metadata | null)[];
privateMetadata: (CollectionDetails_collection_privateMetadata | null)[];
backgroundImage: CollectionDetails_collection_backgroundImage | null;
slug: string;
descriptionJson: any;
publicationDate: any | null;
seoDescription: string | null;
seoTitle: string | null;
products: CollectionDetails_collection_products | null;
}
export interface CollectionDetails_shop_homepageCollection {
__typename: "Collection";
id: string;
}
export interface CollectionDetails_shop {
__typename: "Shop";
homepageCollection: CollectionDetails_shop_homepageCollection | null;
}
export interface CollectionDetails {
collection: CollectionDetails_collection | null;
shop: CollectionDetails_shop;
}
export interface CollectionDetailsVariables {

View file

@ -8,6 +8,19 @@ import { CollectionFilterInput, CollectionSortingInput } from "./../../types/glo
// GraphQL query operation: CollectionList
// ====================================================
export interface CollectionList_collections_edges_node_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
}
export interface CollectionList_collections_edges_node_channelListings {
__typename: "CollectionChannelListing";
isPublished: boolean;
publicationDate: any | null;
channel: CollectionList_collections_edges_node_channelListings_channel;
}
export interface CollectionList_collections_edges_node_products {
__typename: "ProductCountableConnection";
totalCount: number | null;
@ -16,8 +29,8 @@ export interface CollectionList_collections_edges_node_products {
export interface CollectionList_collections_edges_node {
__typename: "Collection";
id: string;
isPublished: boolean;
name: string;
channelListings: CollectionList_collections_edges_node_channelListings[] | null;
products: CollectionList_collections_edges_node_products | null;
}

View file

@ -2,12 +2,25 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { CollectionInput, ProductErrorCode } from "./../../types/globalTypes";
import { CollectionInput, CollectionErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: CollectionUpdate
// ====================================================
export interface CollectionUpdate_collectionUpdate_collection_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
}
export interface CollectionUpdate_collectionUpdate_collection_channelListings {
__typename: "CollectionChannelListing";
isPublished: boolean;
publicationDate: any | null;
channel: CollectionUpdate_collectionUpdate_collection_channelListings_channel;
}
export interface CollectionUpdate_collectionUpdate_collection_metadata {
__typename: "MetadataItem";
key: string;
@ -29,21 +42,20 @@ export interface CollectionUpdate_collectionUpdate_collection_backgroundImage {
export interface CollectionUpdate_collectionUpdate_collection {
__typename: "Collection";
id: string;
isPublished: boolean;
name: string;
channelListings: CollectionUpdate_collectionUpdate_collection_channelListings[] | null;
metadata: (CollectionUpdate_collectionUpdate_collection_metadata | null)[];
privateMetadata: (CollectionUpdate_collectionUpdate_collection_privateMetadata | null)[];
backgroundImage: CollectionUpdate_collectionUpdate_collection_backgroundImage | null;
slug: string;
descriptionJson: any;
publicationDate: any | null;
seoDescription: string | null;
seoTitle: string | null;
}
export interface CollectionUpdate_collectionUpdate_errors {
__typename: "ProductError";
code: ProductErrorCode;
__typename: "CollectionError";
code: CollectionErrorCode;
field: string | null;
}

View file

@ -2,32 +2,23 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { CollectionInput, ShopErrorCode, ProductErrorCode } from "./../../types/globalTypes";
import { CollectionInput, CollectionErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: CollectionUpdateWithHomepage
// ====================================================
export interface CollectionUpdateWithHomepage_homepageCollectionUpdate_errors {
__typename: "ShopError";
code: ShopErrorCode;
field: string | null;
}
export interface CollectionUpdateWithHomepage_homepageCollectionUpdate_shop_homepageCollection {
__typename: "Collection";
export interface CollectionUpdateWithHomepage_collectionUpdate_collection_channelListing_channel {
__typename: "Channel";
id: string;
name: string;
}
export interface CollectionUpdateWithHomepage_homepageCollectionUpdate_shop {
__typename: "Shop";
homepageCollection: CollectionUpdateWithHomepage_homepageCollectionUpdate_shop_homepageCollection | null;
}
export interface CollectionUpdateWithHomepage_homepageCollectionUpdate {
__typename: "HomepageCollectionUpdate";
errors: CollectionUpdateWithHomepage_homepageCollectionUpdate_errors[];
shop: CollectionUpdateWithHomepage_homepageCollectionUpdate_shop | null;
export interface CollectionUpdateWithHomepage_collectionUpdate_collection_channelListing {
__typename: "CollectionChannelListing";
isPublished: boolean;
publicationDate: any | null;
channel: CollectionUpdateWithHomepage_collectionUpdate_collection_channelListing_channel;
}
export interface CollectionUpdateWithHomepage_collectionUpdate_collection_metadata {
@ -51,21 +42,20 @@ export interface CollectionUpdateWithHomepage_collectionUpdate_collection_backgr
export interface CollectionUpdateWithHomepage_collectionUpdate_collection {
__typename: "Collection";
id: string;
isPublished: boolean;
name: string;
channelListings: CollectionUpdateWithHomepage_collectionUpdate_collection_channelListing[] | null;
metadata: (CollectionUpdateWithHomepage_collectionUpdate_collection_metadata | null)[];
privateMetadata: (CollectionUpdateWithHomepage_collectionUpdate_collection_privateMetadata | null)[];
backgroundImage: CollectionUpdateWithHomepage_collectionUpdate_collection_backgroundImage | null;
slug: string;
descriptionJson: any;
publicationDate: any | null;
seoDescription: string | null;
seoTitle: string | null;
}
export interface CollectionUpdateWithHomepage_collectionUpdate_errors {
__typename: "ProductError";
code: ProductErrorCode;
__typename: "CollectionError";
code: CollectionErrorCode;
field: string | null;
}
@ -76,12 +66,10 @@ export interface CollectionUpdateWithHomepage_collectionUpdate {
}
export interface CollectionUpdateWithHomepage {
homepageCollectionUpdate: CollectionUpdateWithHomepage_homepageCollectionUpdate | null;
collectionUpdate: CollectionUpdateWithHomepage_collectionUpdate | null;
}
export interface CollectionUpdateWithHomepageVariables {
id: string;
input: CollectionInput;
homepageId?: string | null;
}

View file

@ -2,12 +2,25 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { CollectionCreateInput, ProductErrorCode } from "./../../types/globalTypes";
import { CollectionCreateInput, CollectionErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: CreateCollection
// ====================================================
export interface CreateCollection_collectionCreate_collection_channelListings_channel {
__typename: "Channel";
id: string;
name: string;
}
export interface CreateCollection_collectionCreate_collection_channelListings {
__typename: "CollectionChannelListing";
isPublished: boolean;
publicationDate: any | null;
channel: CreateCollection_collectionCreate_collection_channelListings_channel;
}
export interface CreateCollection_collectionCreate_collection_metadata {
__typename: "MetadataItem";
key: string;
@ -29,21 +42,20 @@ export interface CreateCollection_collectionCreate_collection_backgroundImage {
export interface CreateCollection_collectionCreate_collection {
__typename: "Collection";
id: string;
isPublished: boolean;
name: string;
channelListings: CreateCollection_collectionCreate_collection_channelListings[] | null;
metadata: (CreateCollection_collectionCreate_collection_metadata | null)[];
privateMetadata: (CreateCollection_collectionCreate_collection_privateMetadata | null)[];
backgroundImage: CreateCollection_collectionCreate_collection_backgroundImage | null;
slug: string;
descriptionJson: any;
publicationDate: any | null;
seoDescription: string | null;
seoTitle: string | null;
}
export interface CreateCollection_collectionCreate_errors {
__typename: "ProductError";
code: ProductErrorCode;
__typename: "CollectionError";
code: CollectionErrorCode;
field: string | null;
}

View file

@ -2,15 +2,15 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ProductErrorCode } from "./../../types/globalTypes";
import { CollectionErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: RemoveCollection
// ====================================================
export interface RemoveCollection_collectionDelete_errors {
__typename: "ProductError";
code: ProductErrorCode;
__typename: "CollectionError";
code: CollectionErrorCode;
field: string | null;
}

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ProductErrorCode } from "./../../types/globalTypes";
import { CollectionErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: UnassignCollectionProduct
@ -22,7 +22,6 @@ export interface UnassignCollectionProduct_collectionRemoveProducts_collection_p
export interface UnassignCollectionProduct_collectionRemoveProducts_collection_products_edges_node {
__typename: "Product";
id: string;
isPublished: boolean;
name: string;
productType: UnassignCollectionProduct_collectionRemoveProducts_collection_products_edges_node_productType;
thumbnail: UnassignCollectionProduct_collectionRemoveProducts_collection_products_edges_node_thumbnail | null;
@ -54,8 +53,8 @@ export interface UnassignCollectionProduct_collectionRemoveProducts_collection {
}
export interface UnassignCollectionProduct_collectionRemoveProducts_errors {
__typename: "ProductError";
code: ProductErrorCode;
__typename: "CollectionError";
code: CollectionErrorCode;
field: string | null;
}

View file

@ -19,11 +19,7 @@ export enum CollectionListUrlFiltersEnum {
query = "query"
}
export type CollectionListUrlFilters = Filters<CollectionListUrlFiltersEnum>;
export type CollectionListUrlDialog =
| "publish"
| "unpublish"
| "remove"
| TabActionDialog;
export type CollectionListUrlDialog = "remove" | "settings" | TabActionDialog;
export enum CollectionListUrlSortField {
name = "name",
available = "available",

21
src/collections/utils.ts Normal file
View file

@ -0,0 +1,21 @@
import { ChannelCollectionData } from "@saleor/channels/utils";
export const createChannelsChangeHandler = (
channelListings: ChannelCollectionData[],
updateChannels: (data: ChannelCollectionData[]) => void,
triggerChange: () => void
) => (id: string, data: Omit<ChannelCollectionData, "name" | "id">) => {
const channelIndex = channelListings.findIndex(channel => channel.id === id);
const channel = channelListings[channelIndex];
const updatedChannels = [
...channelListings.slice(0, channelIndex),
{
...channel,
...data
},
...channelListings.slice(channelIndex + 1)
];
updateChannels(updatedChannels);
triggerChange();
};

View file

@ -1,8 +1,11 @@
import { useChannelsList } from "@saleor/channels/queries";
import { createCollectionChannels } from "@saleor/channels/utils";
import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog";
import { WindowTitle } from "@saleor/components/WindowTitle";
import useChannels from "@saleor/hooks/useChannels";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import getPublicationData from "@saleor/utils/data/getPublicationData";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import {
useMetadataUpdate,
@ -14,7 +17,10 @@ import { useIntl } from "react-intl";
import { CollectionCreateInput } from "../../types/globalTypes";
import CollectionCreatePage from "../components/CollectionCreatePage/CollectionCreatePage";
import { CollectionCreateData } from "../components/CollectionCreatePage/form";
import { useCollectionCreateMutation } from "../mutations";
import {
useCollectionChannelListingUpdate,
useCollectionCreateMutation
} from "../mutations";
import { collectionListUrl, collectionUrl } from "../urls";
export const CollectionCreate: React.FC = () => {
@ -24,6 +30,31 @@ export const CollectionCreate: React.FC = () => {
const [updateMetadata] = useMetadataUpdate({});
const [updatePrivateMetadata] = usePrivateMetadataUpdate({});
const [
updateChannels,
updateChannelsOpts
] = useCollectionChannelListingUpdate({});
const { data: channelsData } = useChannelsList({});
const allChannels = createCollectionChannels(
channelsData?.channels
)?.sort((channel, nextChannel) =>
channel.name.localeCompare(nextChannel.name)
);
const {
channelListElements,
channelsToggle,
currentChannels,
handleChannelsConfirm,
handleChannelsModalClose,
handleChannelsModalOpen,
isChannelSelected,
isChannelsModalOpen,
setCurrentChannels,
toggleAllChannels
} = useChannels(allChannels);
const [createCollection, createCollectionOpts] = useCollectionCreateMutation({
onCompleted: data => {
if (data.collectionCreate.errors.length === 0) {
@ -58,13 +89,29 @@ export const CollectionCreate: React.FC = () => {
seo: {
description: formData.seoDescription,
title: formData.seoTitle
},
...getPublicationData(formData)
}
}
}
});
return result.data?.collectionCreate.collection?.id || null;
const id = result.data?.collectionCreate.collection?.id || null;
if (id) {
updateChannels({
variables: {
id,
input: {
addChannels: formData.channelListings.map(channel => ({
channelId: channel.id,
isPublished: channel.isPublished,
publicationDate: channel.publicationDate
})),
removeChannels: []
}
}
});
}
return id;
};
const handleSubmit = createMetadataCreateHandler(
@ -81,10 +128,34 @@ export const CollectionCreate: React.FC = () => {
description: "window title"
})}
/>
{!!allChannels?.length && (
<ChannelsAvailabilityDialog
isSelected={isChannelSelected}
disabled={!channelListElements.length}
channels={allChannels}
onChange={channelsToggle}
onClose={handleChannelsModalClose}
open={isChannelsModalOpen}
title={intl.formatMessage({
defaultMessage: "Manage Collection Channel Availability"
})}
confirmButtonState="default"
selected={channelListElements.length}
onConfirm={handleChannelsConfirm}
toggleAll={toggleAllChannels}
/>
)}
<CollectionCreatePage
errors={createCollectionOpts.data?.collectionCreate.errors || []}
channelsErrors={
updateChannelsOpts?.data?.collectionChannelListingUpdate.errors || []
}
currentChannels={currentChannels}
channelsCount={channelsData?.channels?.length}
openChannelsModal={handleChannelsModalOpen}
onChannelsChange={setCurrentChannels}
onBack={() => navigate(collectionListUrl())}
disabled={createCollectionOpts.loading}
disabled={createCollectionOpts.loading || updateChannelsOpts.loading}
onSubmit={handleSubmit}
saveButtonBarState={createCollectionOpts.status}
/>

View file

@ -1,11 +1,19 @@
import Button from "@material-ui/core/Button";
import DialogContentText from "@material-ui/core/DialogContentText";
import { useChannelsList } from "@saleor/channels/queries";
import {
createCollectionChannels,
createCollectionChannelsData
} from "@saleor/channels/utils";
import ActionDialog from "@saleor/components/ActionDialog";
import AssignProductDialog from "@saleor/components/AssignProductDialog";
import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA, PAGINATE_BY } from "@saleor/config";
import useBulkActions from "@saleor/hooks/useBulkActions";
import useChannels from "@saleor/hooks/useChannels";
import useLocalStorage from "@saleor/hooks/useLocalStorage";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, {
@ -19,6 +27,7 @@ import {
useMetadataUpdate,
usePrivateMetadataUpdate
} from "@saleor/utils/metadata/updateMetadata";
import { diff } from "fast-array-diff";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -29,9 +38,9 @@ import CollectionDetailsPage from "../components/CollectionDetailsPage/Collectio
import { CollectionUpdateData } from "../components/CollectionDetailsPage/form";
import {
useCollectionAssignProductMutation,
useCollectionChannelListingUpdate,
useCollectionRemoveMutation,
useCollectionUpdateMutation,
useCollectionUpdateWithHomepageMutation,
useUnassignCollectionProductMutation
} from "../mutations";
import { TypedCollectionDetailsQuery } from "../queries";
@ -65,6 +74,12 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
const [updateMetadata] = useMetadataUpdate({});
const [updatePrivateMetadata] = usePrivateMetadataUpdate({});
const [
updateChannels,
updateChannelsOpts
] = useCollectionChannelListingUpdate({});
const { data: channelsData } = useChannelsList({});
const handleCollectionUpdate = (data: CollectionUpdate) => {
if (data.collectionUpdate.errors.length === 0) {
notify({
@ -88,17 +103,6 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
onCompleted: handleCollectionUpdate
});
const [
updateCollectionWithHomepage,
updateCollectionWithHomepageOpts
] = useCollectionUpdateWithHomepageMutation({
onCompleted: data => {
if (data.homepageCollectionUpdate.errors.length === 0) {
handleCollectionUpdate(data);
}
}
});
const [assignProduct, assignProductOpts] = useCollectionAssignProductMutation(
{
onCompleted: data => {
@ -155,6 +159,8 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
const paginationState = createPaginationState(PAGINATE_BY, params);
const handleBack = () => navigate(collectionListUrl());
const [selectedChannel] = useLocalStorage("collectionListChannel", "");
return (
<TypedCollectionDetailsQuery
displayLoader
@ -162,50 +168,72 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
>
{({ data, loading }) => {
const collection = data?.collection;
if (collection === null) {
return <NotFoundPage onBack={handleBack} />;
}
const allChannels = createCollectionChannels(
channelsData?.channels
)?.sort((channel, nextChannel) =>
channel.name.localeCompare(nextChannel.name)
);
const collectionChannelsChoices = createCollectionChannelsData(
collection
);
const {
channelListElements,
channelsToggle,
currentChannels,
handleChannelsConfirm,
handleChannelsModalClose,
handleChannelsModalOpen,
isChannelSelected,
isChannelsModalOpen,
setCurrentChannels,
toggleAllChannels
} = useChannels(collectionChannelsChoices);
const handleUpdate = async (formData: CollectionUpdateData) => {
const input: CollectionInput = {
backgroundImageAlt: formData.backgroundImageAlt,
descriptionJson: JSON.stringify(formData.description),
isPublished: formData.isPublished,
name: formData.name,
publicationDate: formData.publicationDate,
seo: {
description: formData.seoDescription,
title: formData.seoTitle
},
slug: formData.slug
};
const isFeatured = data.shop.homepageCollection
? data.shop.homepageCollection.id === data.collection.id
: false;
if (formData.isFeatured !== isFeatured) {
const result = await updateCollectionWithHomepage({
variables: {
homepageId: formData.isFeatured ? id : null,
id,
input
}
});
return [
...result.data.collectionUpdate.errors,
...result.data.homepageCollectionUpdate.errors
];
} else {
const result = await updateCollection({
variables: {
id,
input
}
});
const result = await updateCollection({
variables: {
id,
input
}
});
const diffChannels = diff(
collectionChannelsChoices,
formData.channelListings,
(a, b) => a.id === b.id
);
return result.data.collectionUpdate.errors;
}
updateChannels({
variables: {
id: collection.id,
input: {
addChannels: formData.channelListings.map(channel => ({
channelId: channel.id,
isPublished: channel.isPublished,
publicationDate: channel.publicationDate
})),
removeChannels:
diffChannels.removed?.map(
removedChannel => removedChannel.id
) || []
}
}
});
return result.data.collectionUpdate.errors;
};
const handleSubmit = createMetadataUpdateHandler(
data?.collection,
@ -215,13 +243,9 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
);
const formTransitionState = getMutationState(
updateCollectionOpts.called ||
updateCollectionWithHomepageOpts.called,
updateCollectionOpts.loading ||
updateCollectionWithHomepageOpts.loading,
updateCollectionOpts.data?.collectionUpdate.errors,
updateCollectionWithHomepageOpts.data?.collectionUpdate.errors,
updateCollectionWithHomepageOpts.data?.homepageCollectionUpdate.errors
updateCollectionOpts.called,
updateCollectionOpts.loading,
updateCollectionOpts.data?.collectionUpdate.errors
);
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
@ -232,17 +256,34 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
return (
<>
<WindowTitle title={maybe(() => data.collection.name)} />
<WindowTitle title={data?.collection?.name} />
{!!allChannels?.length && (
<ChannelsAvailabilityDialog
isSelected={isChannelSelected}
disabled={!channelListElements.length}
channels={allChannels}
onChange={channelsToggle}
onClose={handleChannelsModalClose}
open={isChannelsModalOpen}
title={intl.formatMessage({
defaultMessage: "Manage Collection Channel Availability"
})}
confirmButtonState="default"
selected={channelListElements.length}
onConfirm={handleChannelsConfirm}
toggleAll={toggleAllChannels}
/>
)}
<CollectionDetailsPage
onAdd={() => openModal("assign")}
onBack={handleBack}
disabled={loading}
disabled={loading || updateChannelsOpts.loading}
collection={data?.collection}
channelsErrors={
updateChannelsOpts?.data?.collectionChannelListingUpdate
.errors || []
}
errors={updateCollectionOpts?.data?.collectionUpdate.errors || []}
isFeatured={maybe(
() => data.shop.homepageCollection.id === data.collection.id,
false
)}
onCollectionRemove={() => openModal("remove")}
onImageDelete={() => openModal("removeImage")}
onImageUpload={file =>
@ -290,6 +331,14 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
currentChannels={currentChannels}
hasChannelChanged={
collectionChannelsChoices?.length !== currentChannels?.length
}
channelsCount={channelsData?.channels?.length}
selectedChannel={selectedChannel}
openChannelsModal={handleChannelsModalOpen}
onChannelsChange={setCurrentChannels}
/>
<AssignProductDialog
confirmButtonState={assignProductOpts.status}

View file

@ -1,35 +1,31 @@
import Button from "@material-ui/core/Button";
import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete";
import ChannelSettingsDialog from "@saleor/channels/components/ChannelSettingsDialog";
import ActionDialog from "@saleor/components/ActionDialog";
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
import SaveFilterTabDialog, {
SaveFilterTabDialogFormData
} from "@saleor/components/SaveFilterTabDialog";
import useBulkActions from "@saleor/hooks/useBulkActions";
import useChannelsSettings from "@saleor/hooks/useChannelsSettings";
import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl";
import { maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import createSortHandler from "@saleor/utils/handlers/sortHandler";
import { getSortParams } from "@saleor/utils/sort";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CollectionListPage from "../../components/CollectionListPage/CollectionListPage";
import {
useCollectionBulkDelete,
useCollectionBulkPublish
} from "../../mutations";
import { useCollectionBulkDelete } from "../../mutations";
import { useCollectionListQuery } from "../../queries";
import {
collectionAddUrl,
@ -42,8 +38,6 @@ import {
areFiltersApplied,
deleteFilterTab,
getActiveFilters,
getFilterOpts,
getFilterQueryParam,
getFilterTabs,
getFilterVariables,
saveFilterTab
@ -58,7 +52,6 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
const navigate = useNavigator();
const notify = useNotifier();
const paginate = usePaginator();
const shop = useShop();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids
);
@ -97,24 +90,6 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
}
}
});
const [
collectionBulkPublish,
collectionBulkPublishOpts
] = useCollectionBulkPublish({
onCompleted: data => {
if (data.collectionBulkPublish.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
refetch();
reset();
closeModal();
}
}
});
const tabs = getFilterTabs();
const currentTab =
@ -124,23 +99,27 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
: 0
: parseInt(params.activeTab, 0);
const [
changeFilters,
resetFilters,
handleSearchChange
] = createFilterHandlers({
cleanupFn: reset,
createUrl: collectionListUrl,
getFilterQueryParam,
navigate,
params
});
const handleSearchChange = (query: string) => {
navigate(
collectionListUrl({
...getActiveFilters(params),
activeTab: undefined,
query
})
);
};
const [openModal, closeModal] = createDialogActionHandlers<
CollectionListUrlDialog,
CollectionListUrlQueryParams
>(navigate, collectionListUrl, params);
const {
channelChoices,
handleChannelSelectConfirm,
selectedChannel
} = useChannelsSettings("collectionListChannel", { closeModal, openModal });
const handleTabChange = (tab: number) => {
reset();
navigate(
@ -169,19 +148,25 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
);
const handleSort = createSortHandler(navigate, collectionListUrl, params);
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
return (
<>
{!!channelChoices?.length && (
<ChannelSettingsDialog
channelsChoices={channelChoices}
defaultChoice={selectedChannel}
open={params.action === "settings"}
confirmButtonState="default"
onClose={closeModal}
onConfirm={handleChannelSelectConfirm}
/>
)}
<CollectionListPage
currencySymbol={currencySymbol}
currentTab={currentTab}
filterOpts={getFilterOpts(params)}
initialSearch={params.query || ""}
onSearchChange={handleSearchChange}
onFilterChange={changeFilters}
onAdd={() => navigate(collectionAddUrl)}
onAll={resetFilters}
onAll={() => navigate(collectionListUrl())}
onTabChange={handleTabChange}
onTabDelete={() => openModal("delete-search")}
onTabSave={() => openModal("save-search")}
@ -197,108 +182,27 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
sort={getSortParams(params)}
onRowClick={id => () => navigate(collectionUrl(id))}
toolbar={
<>
<Button
color="primary"
onClick={() =>
openModal("unpublish", {
ids: listElements
})
}
>
<FormattedMessage
defaultMessage="Unpublish"
description="unpublish collections"
/>
</Button>
<Button
color="primary"
onClick={() =>
openModal("publish", {
ids: listElements
})
}
>
<FormattedMessage
defaultMessage="Publish"
description="publish collections"
/>
</Button>
<IconButton
color="primary"
onClick={() =>
openModal("remove", {
ids: listElements
})
}
>
<DeleteIcon />
</IconButton>
</>
<IconButton
color="primary"
onClick={() =>
openModal("remove", {
ids: listElements
})
}
>
<DeleteIcon />
</IconButton>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
channelsCount={channelChoices?.length}
selectedChannel={selectedChannel}
onSettingsOpen={
!!channelChoices?.length ? () => openModal("settings") : undefined
}
/>
<ActionDialog
open={params.action === "publish" && maybe(() => params.ids.length > 0)}
onClose={closeModal}
confirmButtonState={collectionBulkPublishOpts.status}
onConfirm={() =>
collectionBulkPublish({
variables: {
ids: params.ids,
isPublished: true
}
})
}
variant="default"
title={intl.formatMessage({
defaultMessage: "Publish collections",
description: "dialog title"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to publish this collection?} other{Are you sure you want to publish {displayQuantity} collections?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={
params.action === "unpublish" && maybe(() => params.ids.length > 0)
}
onClose={closeModal}
confirmButtonState={collectionBulkPublishOpts.status}
onConfirm={() =>
collectionBulkPublish({
variables: {
ids: params.ids,
isPublished: false
}
})
}
variant="default"
title={intl.formatMessage({
defaultMessage: "Unpublish collections",
description: "dialog title"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to unpublish this collection?} other{Are you sure you want to unpublish {displayQuantity} collections?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "remove" && maybe(() => params.ids.length > 0)}
onClose={closeModal}

View file

@ -31,6 +31,7 @@ interface ActionDialogProps extends DialogProps {
children?: React.ReactNode;
confirmButtonLabel?: string;
confirmButtonState: ConfirmButtonTransitionState;
disabled?: boolean;
maxWidth?: "xs" | "sm" | "md" | "lg" | "xl" | false;
title: string;
variant?: "default" | "delete" | "info";
@ -42,6 +43,7 @@ const ActionDialog: React.FC<ActionDialogProps> = props => {
children,
confirmButtonLabel,
confirmButtonState,
disabled,
open,
title,
variant,
@ -58,11 +60,12 @@ const ActionDialog: React.FC<ActionDialogProps> = props => {
<DialogTitle>{title}</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<Button data-test="back" onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
{variant !== "info" && (
<ConfirmButton
disabled={disabled}
transitionState={confirmButtonState}
color="primary"
variant="contained"
@ -70,6 +73,7 @@ const ActionDialog: React.FC<ActionDialogProps> = props => {
className={classNames({
[classes.deleteButton]: variant === "delete"
})}
data-test="submit"
>
{confirmButtonLabel ||
(variant === "delete"

View file

@ -238,7 +238,7 @@ const useStyles = makeStyles(
padding: 25
},
popover: {
zIndex: 1
zIndex: 2
},
root: {
width: `100%`

View file

@ -1,23 +1,24 @@
import { createChannelsDataFromProduct } from "@saleor/channels/utils";
import { product } from "@saleor/products/fixtures";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
const productChannels = createChannelsDataFromProduct(product(""));
import AvailabilityCard from "./AvailabilityCard";
const props = {
data: {
availableForPurchase: "",
isAvailableForPurchase: false,
isPublished: true,
publicationDate: "",
visibleInListings: true
},
allChannelsCount: 4,
channels: productChannels,
errors: [],
messages: {
hiddenLabel: "Not published",
hiddenSecondLabel: "hidden label",
visibleLabel: "Published"
},
onChange: () => undefined
onChange: () => undefined,
openModal: () => undefined,
selectedChannelsCount: 3
};
storiesOf("Generics / AvailabilityCard", module)

View file

@ -1,51 +1,92 @@
import VisibilityCard, {
VisibilityCardProps
} from "@saleor/components/VisibilityCard";
import ChannelsAvailability, {
ChannelsAvailabilityProps,
Message
} from "@saleor/components/ChannelsAvailability";
import useDateLocalize from "@saleor/hooks/useDateLocalize";
import React from "react";
import { useIntl } from "react-intl";
interface AvailabilityCardProps extends VisibilityCardProps {
data: {
availableForPurchase: string;
isAvailableForPurchase: boolean;
isPublished: boolean;
publicationDate: string;
visibleInListings: boolean;
};
interface AvailabilityCardProps {
messages: Message;
}
export const AvailabilityCard: React.FC<AvailabilityCardProps> = props => {
export const AvailabilityCard: React.FC<AvailabilityCardProps &
Omit<ChannelsAvailabilityProps, "channelsMessages">> = props => {
const intl = useIntl();
const localizeDate = useDateLocalize();
return (
<VisibilityCard
<ChannelsAvailability
{...props}
messages={{
...props.messages,
availableLabel: intl.formatMessage({
defaultMessage: "Available for purchase",
description: "product availability"
}),
availableSecondLabel: intl.formatMessage(
{
defaultMessage: "will become available on {date}",
description: "product available for purchase date"
},
{
date: localizeDate(props.data.availableForPurchase, "L")
channelsMessages={props.channels.reduce(
(prevVal, currVal) => ({
...prevVal,
[currVal.id]: {
...props.messages,
availableDateText:
currVal.publicationDate && !currVal.isPublished
? intl.formatMessage(
{
defaultMessage: "Will become available on {date}",
description: "channel publication date"
},
{
date: localizeDate(currVal.publicationDate, "L")
}
)
: currVal.publicationDate
? intl.formatMessage(
{
defaultMessage: "Visible since {date}",
description: "channel publication date"
},
{
date: localizeDate(currVal.publicationDate, "L")
}
)
: currVal.isPublished
? intl.formatMessage({
defaultMessage: "Visible",
description: "channel publication status"
})
: intl.formatMessage({
defaultMessage: "Hidden",
description: "channel publication status"
}),
availableLabel: intl.formatMessage({
defaultMessage: "Available for purchase",
description: "product availability"
}),
availableSecondLabel: intl.formatMessage(
{
defaultMessage: "will become available on {date}",
description: "product available for purchase date"
},
{
date: localizeDate(currVal.availableForPurchase, "L")
}
),
hiddenSecondLabel: intl.formatMessage(
{
defaultMessage: "will become published on {date}",
description: "product publication date label"
},
{
date: localizeDate(currVal.publicationDate, "L")
}
),
setAvailabilityDateLabel: intl.formatMessage({
defaultMessage: "Set availability date",
description: "product availability date label"
}),
unavailableLabel: intl.formatMessage({
defaultMessage: "Unavailable for purchase",
description: "product unavailability"
})
}
),
setAvailabilityDateLabel: intl.formatMessage({
defaultMessage: "Set availability date",
description: "product availability date label"
}),
unavailableLabel: intl.formatMessage({
defaultMessage: "Unavailable for purchase",
description: "product unavailability"
})
}}
{}
)}
/>
);
};

View file

@ -26,6 +26,9 @@ export interface CardMenuProps {
const useStyles = makeStyles(
theme => ({
container: {
zIndex: 1
},
iconButton: {
background: theme.palette.background.paper,
borderRadius: "100%",
@ -98,6 +101,7 @@ const CardMenu: React.FC<CardMenuProps> = props => {
</IconButton>
<Popper
placement="bottom-end"
className={classes.container}
open={open}
anchorEl={anchorRef.current}
transition

View file

@ -0,0 +1,45 @@
import { createChannelsDataFromProduct } from "@saleor/channels/utils";
import { product } from "@saleor/products/fixtures";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import ChannelsAvailability, {
ChannelsAvailabilityProps
} from "./ChannelsAvailability";
const productChannels = createChannelsDataFromProduct(product(""));
const props: ChannelsAvailabilityProps = {
allChannelsCount: 4,
channelsList: productChannels.map(channel => ({
id: channel.id,
name: channel.name
})),
errors: [],
onChange: () => undefined,
openModal: () => undefined,
selectedChannelsCount: 3
};
storiesOf("Generics / ChannelsAvailability", module)
.addDecorator(Decorator)
.add("default", () => <ChannelsAvailability {...props} />)
.add("with onChange", () => (
<ChannelsAvailability
{...props}
channelsList={undefined}
channels={productChannels}
channelsMessages={productChannels.reduce(
(prevVal, currVal) => ({
...prevVal,
[currVal.id]: {
availableLabel: "Available",
availableSecondLabel: "Will become available",
hiddenSecondLabel: "Will become published"
}
}),
{}
)}
/>
));

View file

@ -0,0 +1,461 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { Channel as ChannelList } from "@saleor/channels/utils";
import CardTitle from "@saleor/components/CardTitle";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import Hr from "@saleor/components/Hr";
import RadioSwitchField from "@saleor/components/RadioSwitchField";
import { CollectionChannelListingErrorFragment } from "@saleor/fragments/types/CollectionChannelListingErrorFragment";
import { ProductChannelListingErrorFragment } from "@saleor/fragments/types/ProductChannelListingErrorFragment";
import useDateLocalize from "@saleor/hooks/useDateLocalize";
import ArrowDropdown from "@saleor/icons/ArrowDropdown";
import { RequireOnlyOne } from "@saleor/misc";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import classNames from "classnames";
import React, { useState } from "react";
import { useIntl } from "react-intl";
import { DateContext } from "../Date/DateContext";
import { useStyles } from "./styles";
export interface ChannelData {
id: string;
isPublished: boolean;
name: string;
publicationDate: string | null;
availableForPurchase?: string;
isAvailableForPurchase?: boolean;
visibleInListings?: boolean;
}
export interface Message {
visibleLabel: string;
hiddenLabel: string;
visibleSecondLabel?: string;
hiddenSecondLabel?: string;
availableDateText?: string;
availableLabel?: string;
unavailableLabel?: string;
availableSecondLabel?: string;
setAvailabilityDateLabel?: string;
}
type Error =
| ProductChannelListingErrorFragment
| CollectionChannelListingErrorFragment;
interface Value {
availableForPurchase?: string;
isAvailableForPurchase?: boolean;
isPublished: boolean;
publicationDate: string | null;
visibleInListings?: boolean;
}
interface ChannelsAvailability {
channels: ChannelData[];
channelsList: ChannelList[];
channelsMessages?: { [id: string]: Message };
errors: Error[];
selectedChannelsCount: number;
allChannelsCount: number;
disabled?: boolean;
onChange?: (id: string, data: Value) => void;
openModal: () => void;
}
export type ChannelsAvailabilityProps = RequireOnlyOne<
ChannelsAvailability,
"channels" | "channelsList"
>;
interface ChannelProps {
disabled?: boolean;
data: ChannelData;
errors: Error[];
messages: Message;
onChange: (id: string, data: Value) => void;
}
const Channel: React.FC<ChannelProps> = ({
data,
disabled,
errors,
messages,
onChange
}) => {
const {
availableForPurchase,
isAvailableForPurchase: isAvailable,
isPublished,
publicationDate,
visibleInListings,
id,
name
} = data;
const formData = {
...(availableForPurchase !== undefined ? { availableForPurchase } : {}),
...(isAvailable !== undefined
? { isAvailableForPurchase: isAvailable }
: {}),
isPublished,
publicationDate,
...(visibleInListings !== undefined ? { visibleInListings } : {})
};
const dateNow = React.useContext(DateContext);
const localizeDate = useDateLocalize();
const hasAvailableProps =
isAvailable !== undefined && availableForPurchase !== undefined;
const [isPublicationDate, setPublicationDate] = useState(
publicationDate === null ? true : false
);
const [isAvailableDate, setAvailableDate] = useState(false);
const [isOpen, setOpen] = useState(false);
const intl = useIntl();
const classes = useStyles({});
const todayDate = localizeDate(new Date(dateNow).toString(), "YYYY-MM-DD");
const visibleMessage = (date: string) =>
intl.formatMessage(
{
defaultMessage: "since {date}",
description: "date"
},
{
date: localizeDate(date, "L")
}
);
const formErrors = getFormErrors(
["availableForPurchaseDate", "publicationDate"],
errors
);
return (
<>
<div className={classes.channelItem}>
<div
data-test="channel-availability-item"
role="button"
className={classes.channelBtn}
onClick={() => setOpen(open => !open)}
>
<div className={classes.channelName}>
<Typography>{name}</Typography>
<ArrowDropdown
className={classNames(classes.arrow, {
[classes.rotate]: isOpen
})}
color="primary"
/>
</div>
<Typography variant="caption">
{messages.availableDateText}
</Typography>
</div>
{isOpen && (
<>
<RadioSwitchField
disabled={disabled}
firstOptionLabel={
<>
<p className={classes.label}>{messages.visibleLabel}</p>
{isPublished &&
publicationDate &&
Date.parse(publicationDate) < dateNow && (
<span className={classes.secondLabel}>
{messages.visibleSecondLabel ||
visibleMessage(publicationDate)}
</span>
)}
</>
}
name="isPublished"
secondOptionLabel={
<>
<p className={classes.label}>{messages.hiddenLabel}</p>
{publicationDate &&
!isPublished &&
Date.parse(publicationDate) >= dateNow && (
<span className={classes.secondLabel}>
{messages.hiddenSecondLabel}
</span>
)}
</>
}
value={isPublished}
onChange={() => {
onChange(id, {
...formData,
isPublished: !isPublished,
publicationDate:
!isPublished && !publicationDate
? todayDate
: publicationDate
});
}}
/>
{!isPublished && (
<>
<Typography
className={classes.setPublicationDate}
onClick={() => setPublicationDate(!isPublicationDate)}
>
{intl.formatMessage({
defaultMessage: "Set publication date"
})}
</Typography>
{isPublicationDate && (
<TextField
error={!!formErrors.publicationDate}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "Publish on",
description: "publish on date"
})}
name={`channel:publicationDate:${id}`}
type="date"
fullWidth={true}
helperText={
formErrors.publicationDate
? getProductErrorMessage(
formErrors.publicationDate,
intl
)
: ""
}
value={publicationDate ? publicationDate : ""}
onChange={e =>
onChange(id, {
...formData,
publicationDate: e.target.value
})
}
className={classes.date}
InputLabelProps={{
shrink: true
}}
/>
)}
</>
)}
{hasAvailableProps && (
<>
<Hr />
<RadioSwitchField
disabled={disabled}
firstOptionLabel={
<>
<p className={classes.label}>{messages.availableLabel}</p>
{isAvailable &&
availableForPurchase &&
Date.parse(availableForPurchase) < dateNow && (
<span className={classes.secondLabel}>
{visibleMessage(availableForPurchase)}
</span>
)}
</>
}
name={`channel:isAvailableForPurchase:${id}`}
secondOptionLabel={
<>
<p className={classes.label}>
{messages.unavailableLabel}
</p>
{availableForPurchase && !isAvailable && (
<span className={classes.secondLabel}>
{messages.availableSecondLabel}
</span>
)}
</>
}
value={isAvailable}
onChange={e => {
const { value } = e.target;
return onChange(id, {
...formData,
availableForPurchase: !value
? null
: availableForPurchase,
isAvailableForPurchase: value
});
}}
/>
{!isAvailable && (
<>
<Typography
className={classes.setPublicationDate}
onClick={() => setAvailableDate(!isAvailableDate)}
>
{messages.setAvailabilityDateLabel}
</Typography>
{isAvailableDate && (
<TextField
error={!!formErrors.availableForPurchaseDate}
disabled={disabled}
label={intl.formatMessage({
defaultMessage: "Set available on",
description: "available on date"
})}
name={`channel:availableForPurchase:${id}`}
type="date"
fullWidth={true}
helperText={
formErrors.availableForPurchaseDate
? getProductErrorMessage(
formErrors.availableForPurchaseDate,
intl
)
: ""
}
value={availableForPurchase ? availableForPurchase : ""}
onChange={e =>
onChange(id, {
...formData,
availableForPurchase: e.target.value
})
}
className={classes.date}
InputLabelProps={{
shrink: true
}}
/>
)}
</>
)}
</>
)}
{visibleInListings !== undefined && (
<>
<Hr />
<ControlledCheckbox
className={classes.checkbox}
name={`channel:visibleInListings:${id}`}
checked={visibleInListings}
disabled={disabled}
label={
<>
<p
className={classNames(
classes.label,
classes.listingLabel
)}
>
{intl.formatMessage({
defaultMessage: "Show in product listings"
})}
</p>
<span className={classes.secondLabel}>
{intl.formatMessage({
defaultMessage:
"Disabling this checkbox will remove product from search and category pages. It will be available on collection pages."
})}
</span>
</>
}
onChange={e =>
onChange(id, {
...formData,
visibleInListings: e.target.value
})
}
/>
</>
)}
</>
)}
</div>
<Hr className={classes.hr} />
</>
);
};
export const ChannelsAvailability: React.FC<ChannelsAvailabilityProps> = props => {
const {
channelsList,
errors,
selectedChannelsCount,
allChannelsCount,
channels,
channelsMessages,
openModal,
onChange
} = props;
const intl = useIntl();
const classes = useStyles({});
const channelsAvailabilityText = intl.formatMessage(
{
defaultMessage:
"Available at {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}",
description: "channels availability text"
},
{
allChannelsCount,
selectedChannelsCount
}
);
return (
<>
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Availability",
description: "section header"
})}
toolbar={
<Button color="primary" onClick={openModal}>
{intl.formatMessage({
defaultMessage: "Manage",
description: "section header button"
})}
</Button>
}
/>
<CardContent className={classes.card}>
{!!channelsAvailabilityText && (
<>
<Typography className={classes.channelInfo}>
{channelsAvailabilityText}
</Typography>
<Hr className={classes.hr} />
</>
)}
{channels
? channels.map(data => {
const channelErrors =
errors?.filter(error => error.channels.includes(data.id)) ||
[];
return (
<Channel
key={data.id}
data={data}
errors={channelErrors}
onChange={onChange}
messages={channelsMessages[data.id]}
/>
);
})
: channelsList
? channelsList.map(data => (
<React.Fragment key={data.id}>
<div className={classes.channelItem}>
<div className={classes.channelName}>
<Typography>{data.name}</Typography>
</div>
</div>
<Hr className={classes.hr} />
</React.Fragment>
))
: null}
</CardContent>
</Card>
</>
);
};
ChannelsAvailability.displayName = "ChannelsAvailability";
export default ChannelsAvailability;

View file

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

View file

@ -0,0 +1,78 @@
import { makeStyles } from "@material-ui/core/styles";
export const useStyles = makeStyles(
theme => ({
arrow: {
transition: theme.transitions.duration.short + "ms"
},
card: {
"&:last-child": {
paddingBottom: 0
},
paddingTop: 0
},
channelBtn: {
"&:focus": {
outline: "none"
},
background: "transparent",
border: "none",
cursor: "pointer",
textAlign: "left"
},
channelInfo: {
fontSize: 14,
padding: theme.spacing(2, 0)
},
channelItem: {
"&:last-child hr": {
display: "none"
},
padding: theme.spacing(2, 0)
},
channelName: {
alignItems: "center",
display: "flex",
justifyContent: "space-between",
marginBottom: theme.spacing(0.5)
},
checkbox: {
alignItems: "flex-start",
marginTop: 10
},
date: {
"& svg": {
fill: theme.palette.primary.main
},
marginTop: theme.spacing(1)
},
hr: {
left: theme.spacing(-3),
position: "relative",
width: `calc(100% + ${theme.spacing(6)}px)`
},
label: {
lineHeight: 1.2,
marginBottom: 5,
marginTop: 0
},
listingLabel: {
marginTop: 9
},
rotate: {
transform: "rotate(180deg)"
},
secondLabel: {
color: theme.palette.text.hint,
fontSize: 12
},
setPublicationDate: {
color: theme.palette.primary.main,
cursor: "pointer",
fontSize: 14,
paddingBottom: 10,
paddingTop: 0
}
}),
{ name: "ChannelsAvailabilityCard" }
);

View file

@ -0,0 +1,108 @@
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { Channel } from "@saleor/channels/utils";
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import Hr from "@saleor/components/Hr";
import { filter } from "fuzzaldrin";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useStyles } from "./styles";
export interface ChannelsAvailabilityContentProps {
isSelected: (option: Channel) => boolean;
channels: Channel[];
contentType?: string;
disabled: boolean;
onChange: (option: Channel) => void;
selected?: number;
toggleAllText?: string;
toggleAll?: (items: Channel[], selected: number) => void;
}
export const ChannelsAvailabilityContent: React.FC<ChannelsAvailabilityContentProps> = ({
isSelected,
channels,
contentType = "",
onChange,
selected = 0,
toggleAll,
toggleAllText
}) => {
const classes = useStyles({});
const intl = useIntl();
const searchText = intl.formatMessage({
defaultMessage: "Search through channels"
});
const [query, onQueryChange] = React.useState("");
const filteredChannels = filter(channels, query, { key: "name" });
return (
<div className={classes.content}>
{!!contentType && (
<Typography className={classes.text} variant="caption">
<FormattedMessage
defaultMessage="Select channels you want for {contentType} to be available on"
values={{ contentType }}
/>
</Typography>
)}
<TextField
name="query"
value={query}
className={classes.input}
onChange={e => onQueryChange(e.target.value)}
label={searchText}
placeholder={searchText}
fullWidth
/>
<div className={classes.dialog}>
{!!toggleAll && (
<>
<ControlledCheckbox
checked={selected !== 0}
name="allchannels"
label={
toggleAllText || (
<Typography className={classes.label}>
<FormattedMessage defaultMessage="Available at all channels" />
</Typography>
)
}
onChange={() => toggleAll(channels, selected)}
/>
<Hr />
</>
)}
<Typography className={classes.contentTitle}>
<FormattedMessage defaultMessage="Channels A to Z" />
</Typography>
<div className={classes.scrollArea}>
{filteredChannels?.length ? (
filteredChannels.map(option => (
<div key={option.id} className={classes.option}>
<ControlledCheckbox
checked={isSelected(option)}
name={option.name}
label={
<Typography className={classes.label}>
{option.name}
</Typography>
}
onChange={() => onChange(option)}
/>
<Hr />
</div>
))
) : (
<div className={classes.notFound}>
<FormattedMessage defaultMessage="No Channels found" />
</div>
)}
</div>
</div>
</div>
);
};
ChannelsAvailabilityContent.displayName = "ChannelsAvailabilityContent";
export default ChannelsAvailabilityContent;

View file

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

View file

@ -0,0 +1,47 @@
import makeStyles from "@material-ui/core/styles/makeStyles";
export const useStyles = makeStyles(
theme => ({
content: {
"& hr": {
left: -24,
position: "relative",
width: "calc(100% + 48px)"
}
},
contentTitle: {
margin: theme.spacing(1, 0)
},
dialog: {
marginBottom: -30,
marginTop: theme.spacing(2)
},
input: {
"& label": {
overflowX: "inherit"
}
},
label: {
fontSize: 14
},
notFound: {
paddingBottom: theme.spacing(2)
},
option: {
"&:last-child": {
"& hr": {
display: "none"
}
},
margin: theme.spacing(1, 0)
},
scrollArea: {
maxHeight: 400,
overflowY: "scroll"
},
text: {
marginBottom: 5
}
}),
{ name: "ChannelsAvailabilityContent" }
);

View file

@ -0,0 +1,32 @@
import { channelsList } from "@saleor/channels/fixtures";
import { createChannelsData } from "@saleor/channels/utils";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import ChannelsAvailabilityDialog, {
ChannelsAvailabilityDialogProps
} from "./ChannelsAvailabilityDialog";
const props: ChannelsAvailabilityDialogProps = {
channels: createChannelsData(channelsList),
confirmButtonState: "default",
disabled: false,
isSelected: () => undefined,
onChange: () => undefined,
onClose: () => undefined,
onConfirm: () => undefined,
open: true,
title: "Channels",
toggleAll: () => undefined
};
storiesOf("Generics / ChannelsAvailabilityDialog", module)
.addDecorator(Decorator)
.add("default", () => <ChannelsAvailabilityDialog {...props} />)
.add("with text", () => (
<ChannelsAvailabilityDialog {...props} contentType="order" />
))
.add("disabled", () => (
<ChannelsAvailabilityDialog {...props} disabled={true} />
));

View file

@ -0,0 +1,56 @@
import { Channel } from "@saleor/channels/utils";
import ActionDialog from "@saleor/components/ActionDialog";
import { ChannelsAvailabilityContent } from "@saleor/components/ChannelsAvailabilityContent";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import React from "react";
export interface ChannelsAvailabilityDialogProps {
isSelected: (option: Channel) => boolean;
channels: Channel[];
confirmButtonState: ConfirmButtonTransitionState;
contentType?: string;
disabled: boolean;
open: boolean;
onClose: () => void;
onChange: (option: Channel) => void;
onConfirm: () => void;
selected?: number;
title: string;
toggleAll?: (items: Channel[], selected: number) => void;
}
export const ChannelsAvailabilityDialog: React.FC<ChannelsAvailabilityDialogProps> = ({
isSelected,
channels,
confirmButtonState,
contentType = "",
disabled,
open,
onClose,
onChange,
onConfirm,
selected = 0,
title,
toggleAll
}) => (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={title}
disabled={disabled}
>
<ChannelsAvailabilityContent
channels={channels}
disabled={disabled}
contentType={contentType}
isSelected={isSelected}
selected={selected}
toggleAll={toggleAll}
onChange={onChange}
/>
</ActionDialog>
);
ChannelsAvailabilityDialog.displayName = "ChannelsAvailabilityDialog";
export default ChannelsAvailabilityDialog;

View file

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

View file

@ -0,0 +1,18 @@
import { productChannels } from "@saleor/channels/fixtures";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import ChannelsAvailabilityDropdown, {
ChannelsAvailabilityDropdownProps
} from "./ChannelsAvailabilityDropdown";
const props: ChannelsAvailabilityDropdownProps = {
allChannelsCount: 6,
channels: productChannels,
currentChannel: productChannels[0]
};
storiesOf("Generics / ChannelsAvailabilityDropdown", module)
.addDecorator(Decorator)
.add("default", () => <ChannelsAvailabilityDropdown {...props} />);

View file

@ -0,0 +1,138 @@
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import Typography from "@material-ui/core/Typography";
import { CollectionList_collections_edges_node_channelListings } from "@saleor/collections/types/CollectionList";
import Hr from "@saleor/components/Hr";
import StatusLabel from "@saleor/components/StatusLabel";
import useDateLocalize from "@saleor/hooks/useDateLocalize";
import { ProductList_products_edges_node_channelListings } from "@saleor/products/types/ProductList";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useStyles } from "./styles";
type Channels =
| ProductList_products_edges_node_channelListings
| CollectionList_collections_edges_node_channelListings;
export interface ChannelsAvailabilityDropdownProps {
allChannelsCount: number;
channels: Channels[];
currentChannel: Channels;
}
const isActive = (channelData: Channels) => channelData?.isPublished;
export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdownProps> = ({
allChannelsCount,
channels,
currentChannel
}) => {
const intl = useIntl();
const classes = useStyles({});
const localizeDate = useDateLocalize();
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = event => setAnchorEl(event.currentTarget);
const handleClose = () => setAnchorEl(null);
return (
<div onClick={e => e.stopPropagation()}>
<div
aria-controls="availability-menu"
aria-haspopup="true"
role="button"
onClick={handleClick}
>
<StatusLabel
label={intl.formatMessage(
{
defaultMessage: "Available in {count}/{allCount}",
description: "product status title"
},
{
allCount: allChannelsCount,
count: channels.length
}
)}
status={isActive(currentChannel) ? "success" : "error"}
/>
</div>
<Menu
id="availability-menu"
anchorEl={anchorEl}
keepMounted
elevation={3}
open={Boolean(anchorEl)}
onClose={handleClose}
getContentAnchorEl={null}
anchorOrigin={{
horizontal: "center",
vertical: "bottom"
}}
transformOrigin={{
horizontal: "center",
vertical: "top"
}}
>
<Typography className={classes.title}>
<FormattedMessage
defaultMessage="Available in {count} out of {allCount, plural, one {# channel} other {# channels}}"
description="product status"
values={{
allCount: allChannelsCount,
count: channels.length
}}
/>
</Typography>
<Hr className={classes.hr} />
{channels.map(channelData => {
const notPublishedText = intl.formatMessage(
{
defaultMessage: "Will become available on {date}",
description: "product channel publication date"
},
{
date: localizeDate(channelData.publicationDate, "L")
}
);
const publishedText = intl.formatMessage(
{
defaultMessage: "published since {date}",
description: "product channel publication date"
},
{
date: localizeDate(channelData.publicationDate, "L")
}
);
return (
<MenuItem key={channelData.channel.id} className={classes.menuItem}>
<StatusLabel
label={channelData.channel.name}
status={isActive(channelData) ? "success" : "error"}
/>
<div>
<Typography variant="caption" className={classes.caption}>
{channelData.isPublished && channelData.publicationDate
? publishedText
: channelData.publicationDate && !channelData.isPublished
? notPublishedText
: channelData.isPublished
? ""
: intl.formatMessage({
defaultMessage: "hidden",
description: "product channel publication status"
})}
</Typography>
</div>
</MenuItem>
);
})}
</Menu>
</div>
);
};
ChannelsAvailabilityDropdown.displayName = "ChannelsAvailabilityDropdown";
export default ChannelsAvailabilityDropdown;

Some files were not shown because too many files have changed in this diff Show more