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", firstCategoryItem: "#downshift-0-item-0",
visibleRadioBtn: "[name='isPublished']", visibleRadioBtn: "[name='isPublished']",
saveBtn: "[data-test='button-bar-confirm']", 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 { LEFT_MENU_SELECTORS } from "../elements/account/left-menu/left-menu-selectors";
import { PRODUCTS_SELECTORS } from "../elements/catalog/product-selectors"; import { PRODUCTS_SELECTORS } from "../elements/catalog/product-selectors";
import { BUTTON_SELECTORS } from "../elements/shared/button-selectors";
// <reference types="cypress" /> // <reference types="cypress" />
describe("Products", () => { describe("Products", () => {
@ -13,6 +14,8 @@ describe("Products", () => {
.click() .click()
.get(PRODUCTS_SELECTORS.products) .get(PRODUCTS_SELECTORS.products)
.click() .click()
.get(BUTTON_SELECTORS.submit)
.click()
.get(PRODUCTS_SELECTORS.createProductBtn) .get(PRODUCTS_SELECTORS.createProductBtn)
.click() .click()
.get(PRODUCTS_SELECTORS.productNameInput) .get(PRODUCTS_SELECTORS.productNameInput)
@ -33,6 +36,9 @@ describe("Products", () => {
.get(PRODUCTS_SELECTORS.categoryItem) .get(PRODUCTS_SELECTORS.categoryItem)
.first() .first()
.click() .click()
.get(PRODUCTS_SELECTORS.channelAvailabilityItem)
.first()
.click()
.get(PRODUCTS_SELECTORS.visibleRadioBtn) .get(PRODUCTS_SELECTORS.visibleRadioBtn)
.first() .first()
.click() .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=", "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=",
"dev": true "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": { "cyclist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/find-with-regex/-/find-with-regex-1.1.3.tgz",
"integrity": "sha512-zkEVQ1H3PIQL/19ADKt1lCQU4QGM3OneiderUcFgn5EgTm/TnoUh7HxPAwP8w/vXxWSLC6KtpbDQpypJ5+majw==" "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": { "flat-cache": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
@ -12566,8 +12580,7 @@
}, },
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -12585,13 +12598,11 @@
}, },
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -12604,18 +12615,15 @@
}, },
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -12718,8 +12726,7 @@
}, },
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -12729,7 +12736,6 @@
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -12742,20 +12748,17 @@
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
}, },
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -12772,7 +12775,6 @@
"mkdirp": { "mkdirp": {
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -12845,8 +12847,7 @@
}, },
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -12856,7 +12857,6 @@
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -12932,8 +12932,7 @@
}, },
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -12963,7 +12962,6 @@
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -12981,7 +12979,6 @@
"strip-ansi": { "strip-ansi": {
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -13020,13 +13017,11 @@
}, },
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true
"optional": true
} }
} }
}, },
@ -17570,6 +17565,11 @@
"boolbase": "~1.0.0" "boolbase": "~1.0.0"
} }
}, },
"nub": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/nub/-/nub-0.0.0.tgz",
"integrity": "sha1-s2m9Mr3eZq9ZYFw7BSC8IZ3MwE8="
},
"num2fraction": { "num2fraction": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",

View file

@ -37,6 +37,7 @@
"apollo-upload-client": "^9.1.0", "apollo-upload-client": "^9.1.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"crc-32": "^1.2.0", "crc-32": "^1.2.0",
"currency-codes": "^2.1.0",
"downshift": "^1.31.16", "downshift": "^1.31.16",
"draft-js": "^0.10.5", "draft-js": "^0.10.5",
"draftail": "^1.2.1", "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 navigate = useNavigator();
const { updateListSettings, settings } = useListSettings(ListViews.APPS_LIST); const { updateListSettings, settings } = useListSettings(ListViews.APPS_LIST);
const paginate = usePaginator(); const paginate = usePaginator();
const paginationState = createPaginationState(settings.rowNumber, params); const paginationState = createPaginationState(settings?.rowNumber, params);
const queryVariables = { const queryVariables = {
sort: { sort: {
direction: OrderDirection.DESC, direction: OrderDirection.DESC,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -85,47 +85,37 @@ export interface CategoryDetails_category_products_edges_node_productType {
name: string; name: string;
} }
export interface CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_start_gross { export interface CategoryDetails_category_products_edges_node_channelListings_discountedPrice {
__typename: "Money"; __typename: "Money";
amount: number; amount: number;
currency: string; currency: string;
} }
export interface CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_start { export interface CategoryDetails_category_products_edges_node_channelListings_channel {
__typename: "TaxedMoney"; __typename: "Channel";
gross: CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_start_gross; id: string;
name: string;
currencyCode: string;
} }
export interface CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_stop_gross { export interface CategoryDetails_category_products_edges_node_channelListings {
__typename: "Money"; __typename: "ProductChannelListing";
amount: number; isPublished: boolean;
currency: string; publicationDate: any | null;
} discountedPrice: CategoryDetails_category_products_edges_node_channelListings_discountedPrice | null;
isAvailableForPurchase: boolean | null;
export interface CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_stop { availableForPurchase: any | null;
__typename: "TaxedMoney"; visibleInListings: boolean;
gross: CategoryDetails_category_products_edges_node_pricing_priceRangeUndiscounted_stop_gross; channel: CategoryDetails_category_products_edges_node_channelListings_channel;
}
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 { export interface CategoryDetails_category_products_edges_node {
__typename: "Product"; __typename: "Product";
id: string; id: string;
name: string; name: string;
isAvailable: boolean | null;
thumbnail: CategoryDetails_category_products_edges_node_thumbnail | null; thumbnail: CategoryDetails_category_products_edges_node_thumbnail | null;
productType: CategoryDetails_category_products_edges_node_productType; 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 { export interface CategoryDetails_category_products_edges {

View file

@ -1,6 +1,7 @@
import DialogContentText from "@material-ui/core/DialogContentText"; import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import { useChannelsList } from "@saleor/channels/queries";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import NotFoundPage from "@saleor/components/NotFoundPage"; import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
@ -78,6 +79,13 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
variables: { ...paginationState, id } variables: { ...paginationState, id }
}); });
const { data: channelsData } = useChannelsList({});
const channelChoices = channelsData?.channels?.map(channel => ({
label: channel.name,
value: channel.id
}));
const category = data?.category; const category = data?.category;
if (category === null) { if (category === null) {
@ -205,6 +213,8 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
<> <>
<WindowTitle title={maybe(() => data.category.name)} /> <WindowTitle title={maybe(() => data.category.name)} />
<CategoryUpdatePage <CategoryUpdatePage
channelsCount={channelsData?.channels?.length}
channelChoices={channelChoices}
changeTab={changeTab} changeTab={changeTab}
currentTab={params.activeTab} currentTab={params.activeTab}
category={maybe(() => data.category)} 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 AppHeader from "@saleor/components/AppHeader";
import { AvailabilityCard } from "@saleor/components/AvailabilityCard";
import { CardSpacer } from "@saleor/components/CardSpacer"; import { CardSpacer } from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import { Container } from "@saleor/components/Container"; import { Container } from "@saleor/components/Container";
@ -7,9 +9,8 @@ import Metadata from "@saleor/components/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm"; import SeoForm from "@saleor/components/SeoForm";
import VisibilityCard from "@saleor/components/VisibilityCard"; import { CollectionChannelListingErrorFragment } from "@saleor/fragments/types/CollectionChannelListingErrorFragment";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { CollectionErrorFragment } from "@saleor/fragments/types/CollectionErrorFragment";
import useDateLocalize from "@saleor/hooks/useDateLocalize";
import { SubmitPromise } from "@saleor/hooks/useForm"; import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import React from "react"; import React from "react";
@ -20,25 +21,38 @@ import { CollectionImage } from "../CollectionImage/CollectionImage";
import CollectionCreateForm, { CollectionCreateData } from "./form"; import CollectionCreateForm, { CollectionCreateData } from "./form";
export interface CollectionCreatePageProps { export interface CollectionCreatePageProps {
channelsCount: number;
channelsErrors: CollectionChannelListingErrorFragment[];
currentChannels: ChannelCollectionData[];
disabled: boolean; disabled: boolean;
errors: ProductErrorFragment[]; errors: CollectionErrorFragment[];
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void; onBack: () => void;
onSubmit: (data: CollectionCreateData) => SubmitPromise; onSubmit: (data: CollectionCreateData) => SubmitPromise;
onChannelsChange: (data: ChannelCollectionData[]) => void;
openChannelsModal: () => void;
} }
const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
channelsCount,
channelsErrors,
currentChannels = [],
disabled, disabled,
errors, errors,
saveButtonBarState, saveButtonBarState,
onBack, onBack,
onChannelsChange,
openChannelsModal,
onSubmit onSubmit
}: CollectionCreatePageProps) => { }: CollectionCreatePageProps) => {
const intl = useIntl(); const intl = useIntl();
const localizeDate = useDateLocalize();
return ( return (
<CollectionCreateForm onSubmit={onSubmit}> <CollectionCreateForm
onSubmit={onSubmit}
currentChannels={currentChannels}
setChannels={onChannelsChange}
>
{({ change, data, handlers, hasChanged, submit }) => ( {({ change, data, handlers, hasChanged, submit }) => (
<Container> <Container>
<AppHeader onBack={onBack}> <AppHeader onBack={onBack}>
@ -115,30 +129,25 @@ const CollectionCreatePage: React.FC<CollectionCreatePageProps> = ({
<Metadata data={data} onChange={handlers.changeMetadata} /> <Metadata data={data} onChange={handlers.changeMetadata} />
</div> </div>
<div> <div>
<VisibilityCard <AvailabilityCard
data={data}
errors={errors}
disabled={disabled}
messages={{ messages={{
hiddenLabel: intl.formatMessage({ hiddenLabel: intl.formatMessage({
defaultMessage: "Hidden", defaultMessage: "Hidden",
description: "collection label" description: "collection label"
}), }),
hiddenSecondLabel: intl.formatMessage(
{
defaultMessage: "will be visible from {date}",
description: "collection"
},
{
date: localizeDate(data.publicationDate, "L")
}
),
visibleLabel: intl.formatMessage({ visibleLabel: intl.formatMessage({
defaultMessage: "Visible", defaultMessage: "Visible",
description: "collection label" description: "collection label"
}) })
}} }}
onChange={change} errors={channelsErrors}
selectedChannelsCount={data.channelListings.length}
allChannelsCount={channelsCount}
channels={data.channelListings}
disabled={disabled}
onChange={handlers.changeChannels}
openModal={openChannelsModal}
/> />
</div> </div>
</Grid> </Grid>

View file

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

View file

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

View file

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

View file

@ -1,9 +1,10 @@
import { OutputData } from "@editorjs/editorjs"; import { OutputData } from "@editorjs/editorjs";
import { ChannelCollectionData } from "@saleor/channels/utils";
import { CollectionDetails_collection } from "@saleor/collections/types/CollectionDetails"; import { CollectionDetails_collection } from "@saleor/collections/types/CollectionDetails";
import { createChannelsChangeHandler } from "@saleor/collections/utils";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
import useForm, { FormChange } from "@saleor/hooks/useForm"; import useForm, { FormChange } from "@saleor/hooks/useForm";
import getPublicationData from "@saleor/utils/data/getPublicationData";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata"; import getMetadata from "@saleor/utils/metadata/getMetadata";
@ -13,13 +14,11 @@ import React from "react";
export interface CollectionUpdateFormData extends MetadataFormData { export interface CollectionUpdateFormData extends MetadataFormData {
backgroundImageAlt: string; backgroundImageAlt: string;
channelListings: ChannelCollectionData[];
name: string; name: string;
slug: string; slug: string;
publicationDate: string;
seoDescription: string; seoDescription: string;
seoTitle: string; seoTitle: string;
isFeatured: boolean;
isPublished: boolean;
} }
export interface CollectionUpdateData extends CollectionUpdateFormData { export interface CollectionUpdateData extends CollectionUpdateFormData {
description: OutputData; description: OutputData;
@ -28,6 +27,10 @@ export interface CollectionUpdateData extends CollectionUpdateFormData {
interface CollectionUpdateHandlers { interface CollectionUpdateHandlers {
changeMetadata: FormChange; changeMetadata: FormChange;
changeDescription: RichTextEditorChange; changeDescription: RichTextEditorChange;
changeChannels: (
id: string,
data: Omit<ChannelCollectionData, "name" | "id">
) => void;
} }
export interface UseCollectionUpdateFormResult { export interface UseCollectionUpdateFormResult {
change: FormChange; change: FormChange;
@ -40,26 +43,26 @@ export interface UseCollectionUpdateFormResult {
export interface CollectionUpdateFormProps { export interface CollectionUpdateFormProps {
children: (props: UseCollectionUpdateFormResult) => React.ReactNode; children: (props: UseCollectionUpdateFormResult) => React.ReactNode;
collection: CollectionDetails_collection; collection: CollectionDetails_collection;
isFeatured: boolean; currentChannels: ChannelCollectionData[];
setChannels: (data: ChannelCollectionData[]) => void;
onSubmit: (data: CollectionUpdateData) => Promise<any[]>; onSubmit: (data: CollectionUpdateData) => Promise<any[]>;
} }
function useCollectionUpdateForm( function useCollectionUpdateForm(
collection: CollectionDetails_collection, collection: CollectionDetails_collection,
onSubmit: (data: CollectionUpdateData) => Promise<any[]>, currentChannels: ChannelCollectionData[],
isFeatured: boolean setChannels: (data: ChannelCollectionData[]) => void,
onSubmit: (data: CollectionUpdateData) => Promise<any[]>
): UseCollectionUpdateFormResult { ): UseCollectionUpdateFormResult {
const [changed, setChanged] = React.useState(false); const [changed, setChanged] = React.useState(false);
const triggerChange = () => setChanged(true); const triggerChange = () => setChanged(true);
const form = useForm<CollectionUpdateFormData>({ const form = useForm<CollectionUpdateFormData>({
backgroundImageAlt: collection?.backgroundImage?.alt || "", backgroundImageAlt: collection?.backgroundImage?.alt || "",
isFeatured, channelListings: currentChannels,
isPublished: !!collection?.isPublished,
metadata: collection?.metadata?.map(mapMetadataItemToInput), metadata: collection?.metadata?.map(mapMetadataItemToInput),
name: collection?.name || "", name: collection?.name || "",
privateMetadata: collection?.privateMetadata?.map(mapMetadataItemToInput), privateMetadata: collection?.privateMetadata?.map(mapMetadataItemToInput),
publicationDate: collection?.publicationDate || "",
seoDescription: collection?.seoDescription || "", seoDescription: collection?.seoDescription || "",
seoTitle: collection?.seoTitle || "", seoTitle: collection?.seoTitle || "",
slug: collection?.slug || "" slug: collection?.slug || ""
@ -89,16 +92,22 @@ function useCollectionUpdateForm(
const getSubmitData = (): CollectionUpdateData => ({ const getSubmitData = (): CollectionUpdateData => ({
...getData(), ...getData(),
...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified), ...getMetadata(form.data, isMetadataModified, isPrivateMetadataModified)
...getPublicationData(form.data)
}); });
const handleChannelChange = createChannelsChangeHandler(
currentChannels,
setChannels,
triggerChange
);
const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged); const submit = () => handleFormSubmit(getSubmitData(), onSubmit, setChanged);
return { return {
change: handleChange, change: handleChange,
data: getData(), data: getData(),
handlers: { handlers: {
changeChannels: handleChannelChange,
changeDescription, changeDescription,
changeMetadata changeMetadata
}, },
@ -108,12 +117,18 @@ function useCollectionUpdateForm(
} }
const CollectionUpdateForm: React.FC<CollectionUpdateFormProps> = ({ const CollectionUpdateForm: React.FC<CollectionUpdateFormProps> = ({
children,
collection, collection,
isFeatured, currentChannels,
setChannels,
children,
onSubmit onSubmit
}) => { }) => {
const props = useCollectionUpdateForm(collection, onSubmit, isFeatured); const props = useCollectionUpdateForm(
collection,
currentChannels,
setChannels,
onSubmit
);
return <form onSubmit={props.submit}>{children(props)}</form>; 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 TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import { CollectionListUrlSortField } from "@saleor/collections/urls"; import { CollectionListUrlSortField } from "@saleor/collections/urls";
import { ChannelsAvailabilityDropdown } from "@saleor/components/ChannelsAvailabilityDropdown";
import Checkbox from "@saleor/components/Checkbox"; import Checkbox from "@saleor/components/Checkbox";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; import ResponsiveTable from "@saleor/components/ResponsiveTable";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableCellHeader from "@saleor/components/TableCellHeader"; import TableCellHeader from "@saleor/components/TableCellHeader";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
@ -15,7 +15,7 @@ import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps, SortPage } from "@saleor/types"; import { ListActions, ListProps, SortPage } from "@saleor/types";
import { getArrowDirection } from "@saleor/utils/sort"; import { getArrowDirection } from "@saleor/utils/sort";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage } from "react-intl";
import { CollectionList_collections_edges_node } from "../../types/CollectionList"; import { CollectionList_collections_edges_node } from "../../types/CollectionList";
@ -49,12 +49,15 @@ interface CollectionListProps
ListActions, ListActions,
SortPage<CollectionListUrlSortField> { SortPage<CollectionListUrlSortField> {
collections: CollectionList_collections_edges_node[]; collections: CollectionList_collections_edges_node[];
channelsCount: number;
selectedChannel: string;
} }
const numberOfColumns = 4; const numberOfColumns = 4;
const CollectionList: React.FC<CollectionListProps> = props => { const CollectionList: React.FC<CollectionListProps> = props => {
const { const {
channelsCount,
collections, collections,
disabled, disabled,
settings, settings,
@ -67,13 +70,13 @@ const CollectionList: React.FC<CollectionListProps> = props => {
pageInfo, pageInfo,
isChecked, isChecked,
selected, selected,
selectedChannel,
toggle, toggle,
toggleAll, toggleAll,
toolbar toolbar
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl();
return ( return (
<ResponsiveTable> <ResponsiveTable>
@ -143,6 +146,9 @@ const CollectionList: React.FC<CollectionListProps> = props => {
collections, collections,
collection => { collection => {
const isSelected = collection ? isChecked(collection.id) : false; const isSelected = collection ? isChecked(collection.id) : false;
const channel = collection?.channelListings.find(
listing => listing.channel.id === selectedChannel
);
return ( return (
<TableRow <TableRow
className={classes.tableRow} className={classes.tableRow}
@ -172,26 +178,18 @@ const CollectionList: React.FC<CollectionListProps> = props => {
</TableCell> </TableCell>
<TableCell <TableCell
className={classes.colAvailability} className={classes.colAvailability}
data-test="published" data-test="availability"
data-test-published={maybe(() => collection.isPublished)} data-test-availability={!!collection?.channelListings?.length}
> >
{maybe( {collection && !collection?.channelListings?.length ? (
() => ( "-"
<StatusLabel ) : collection?.channelListings !== undefined ? (
status={collection.isPublished ? "success" : "error"} <ChannelsAvailabilityDropdown
label={ allChannelsCount={channelsCount}
collection.isPublished currentChannel={channel}
? intl.formatMessage({ channels={collection?.channelListings}
defaultMessage: "Published", />
description: "collection is published" ) : (
})
: intl.formatMessage({
defaultMessage: "Not published",
description: "collection is not published"
})
}
/>
),
<Skeleton /> <Skeleton />
)} )}
</TableCell> </TableCell>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { ProductErrorCode } from "./../../types/globalTypes"; import { CollectionErrorCode } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL mutation operation: CollectionAssignProduct // GraphQL mutation operation: CollectionAssignProduct
@ -19,13 +19,37 @@ export interface CollectionAssignProduct_collectionAddProducts_collection_produc
url: string; 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 { export interface CollectionAssignProduct_collectionAddProducts_collection_products_edges_node {
__typename: "Product"; __typename: "Product";
id: string; id: string;
isPublished: boolean;
name: string; name: string;
productType: CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_productType; productType: CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_productType;
thumbnail: CollectionAssignProduct_collectionAddProducts_collection_products_edges_node_thumbnail | null; 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 { export interface CollectionAssignProduct_collectionAddProducts_collection_products_edges {
@ -54,8 +78,8 @@ export interface CollectionAssignProduct_collectionAddProducts_collection {
} }
export interface CollectionAssignProduct_collectionAddProducts_errors { export interface CollectionAssignProduct_collectionAddProducts_errors {
__typename: "ProductError"; __typename: "CollectionError";
code: ProductErrorCode; code: CollectionErrorCode;
field: string | null; 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 // 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 { export interface CollectionDetails_collection_metadata {
__typename: "MetadataItem"; __typename: "MetadataItem";
key: string; key: string;
@ -35,13 +48,37 @@ export interface CollectionDetails_collection_products_edges_node_thumbnail {
url: string; 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 { export interface CollectionDetails_collection_products_edges_node {
__typename: "Product"; __typename: "Product";
id: string; id: string;
isPublished: boolean;
name: string; name: string;
productType: CollectionDetails_collection_products_edges_node_productType; productType: CollectionDetails_collection_products_edges_node_productType;
thumbnail: CollectionDetails_collection_products_edges_node_thumbnail | null; thumbnail: CollectionDetails_collection_products_edges_node_thumbnail | null;
channelListings: CollectionDetails_collection_products_edges_node_channelListings[] | null;
} }
export interface CollectionDetails_collection_products_edges { export interface CollectionDetails_collection_products_edges {
@ -66,32 +103,20 @@ export interface CollectionDetails_collection_products {
export interface CollectionDetails_collection { export interface CollectionDetails_collection {
__typename: "Collection"; __typename: "Collection";
id: string; id: string;
isPublished: boolean;
name: string; name: string;
channelListings: CollectionDetails_collection_channelListings[] | null;
metadata: (CollectionDetails_collection_metadata | null)[]; metadata: (CollectionDetails_collection_metadata | null)[];
privateMetadata: (CollectionDetails_collection_privateMetadata | null)[]; privateMetadata: (CollectionDetails_collection_privateMetadata | null)[];
backgroundImage: CollectionDetails_collection_backgroundImage | null; backgroundImage: CollectionDetails_collection_backgroundImage | null;
slug: string; slug: string;
descriptionJson: any; descriptionJson: any;
publicationDate: any | null;
seoDescription: string | null; seoDescription: string | null;
seoTitle: string | null; seoTitle: string | null;
products: CollectionDetails_collection_products | 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 { export interface CollectionDetails {
collection: CollectionDetails_collection | null; collection: CollectionDetails_collection | null;
shop: CollectionDetails_shop;
} }
export interface CollectionDetailsVariables { export interface CollectionDetailsVariables {

View file

@ -8,6 +8,19 @@ import { CollectionFilterInput, CollectionSortingInput } from "./../../types/glo
// GraphQL query operation: CollectionList // 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 { export interface CollectionList_collections_edges_node_products {
__typename: "ProductCountableConnection"; __typename: "ProductCountableConnection";
totalCount: number | null; totalCount: number | null;
@ -16,8 +29,8 @@ export interface CollectionList_collections_edges_node_products {
export interface CollectionList_collections_edges_node { export interface CollectionList_collections_edges_node {
__typename: "Collection"; __typename: "Collection";
id: string; id: string;
isPublished: boolean;
name: string; name: string;
channelListings: CollectionList_collections_edges_node_channelListings[] | null;
products: CollectionList_collections_edges_node_products | null; products: CollectionList_collections_edges_node_products | null;
} }

View file

@ -2,12 +2,25 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // 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 // 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 { export interface CollectionUpdate_collectionUpdate_collection_metadata {
__typename: "MetadataItem"; __typename: "MetadataItem";
key: string; key: string;
@ -29,21 +42,20 @@ export interface CollectionUpdate_collectionUpdate_collection_backgroundImage {
export interface CollectionUpdate_collectionUpdate_collection { export interface CollectionUpdate_collectionUpdate_collection {
__typename: "Collection"; __typename: "Collection";
id: string; id: string;
isPublished: boolean;
name: string; name: string;
channelListings: CollectionUpdate_collectionUpdate_collection_channelListings[] | null;
metadata: (CollectionUpdate_collectionUpdate_collection_metadata | null)[]; metadata: (CollectionUpdate_collectionUpdate_collection_metadata | null)[];
privateMetadata: (CollectionUpdate_collectionUpdate_collection_privateMetadata | null)[]; privateMetadata: (CollectionUpdate_collectionUpdate_collection_privateMetadata | null)[];
backgroundImage: CollectionUpdate_collectionUpdate_collection_backgroundImage | null; backgroundImage: CollectionUpdate_collectionUpdate_collection_backgroundImage | null;
slug: string; slug: string;
descriptionJson: any; descriptionJson: any;
publicationDate: any | null;
seoDescription: string | null; seoDescription: string | null;
seoTitle: string | null; seoTitle: string | null;
} }
export interface CollectionUpdate_collectionUpdate_errors { export interface CollectionUpdate_collectionUpdate_errors {
__typename: "ProductError"; __typename: "CollectionError";
code: ProductErrorCode; code: CollectionErrorCode;
field: string | null; field: string | null;
} }

View file

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

View file

@ -2,12 +2,25 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // 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 // 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 { export interface CreateCollection_collectionCreate_collection_metadata {
__typename: "MetadataItem"; __typename: "MetadataItem";
key: string; key: string;
@ -29,21 +42,20 @@ export interface CreateCollection_collectionCreate_collection_backgroundImage {
export interface CreateCollection_collectionCreate_collection { export interface CreateCollection_collectionCreate_collection {
__typename: "Collection"; __typename: "Collection";
id: string; id: string;
isPublished: boolean;
name: string; name: string;
channelListings: CreateCollection_collectionCreate_collection_channelListings[] | null;
metadata: (CreateCollection_collectionCreate_collection_metadata | null)[]; metadata: (CreateCollection_collectionCreate_collection_metadata | null)[];
privateMetadata: (CreateCollection_collectionCreate_collection_privateMetadata | null)[]; privateMetadata: (CreateCollection_collectionCreate_collection_privateMetadata | null)[];
backgroundImage: CreateCollection_collectionCreate_collection_backgroundImage | null; backgroundImage: CreateCollection_collectionCreate_collection_backgroundImage | null;
slug: string; slug: string;
descriptionJson: any; descriptionJson: any;
publicationDate: any | null;
seoDescription: string | null; seoDescription: string | null;
seoTitle: string | null; seoTitle: string | null;
} }
export interface CreateCollection_collectionCreate_errors { export interface CreateCollection_collectionCreate_errors {
__typename: "ProductError"; __typename: "CollectionError";
code: ProductErrorCode; code: CollectionErrorCode;
field: string | null; field: string | null;
} }

View file

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

View file

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

View file

@ -19,11 +19,7 @@ export enum CollectionListUrlFiltersEnum {
query = "query" query = "query"
} }
export type CollectionListUrlFilters = Filters<CollectionListUrlFiltersEnum>; export type CollectionListUrlFilters = Filters<CollectionListUrlFiltersEnum>;
export type CollectionListUrlDialog = export type CollectionListUrlDialog = "remove" | "settings" | TabActionDialog;
| "publish"
| "unpublish"
| "remove"
| TabActionDialog;
export enum CollectionListUrlSortField { export enum CollectionListUrlSortField {
name = "name", name = "name",
available = "available", 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 { WindowTitle } from "@saleor/components/WindowTitle";
import useChannels from "@saleor/hooks/useChannels";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import getPublicationData from "@saleor/utils/data/getPublicationData";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import { import {
useMetadataUpdate, useMetadataUpdate,
@ -14,7 +17,10 @@ import { useIntl } from "react-intl";
import { CollectionCreateInput } from "../../types/globalTypes"; import { CollectionCreateInput } from "../../types/globalTypes";
import CollectionCreatePage from "../components/CollectionCreatePage/CollectionCreatePage"; import CollectionCreatePage from "../components/CollectionCreatePage/CollectionCreatePage";
import { CollectionCreateData } from "../components/CollectionCreatePage/form"; import { CollectionCreateData } from "../components/CollectionCreatePage/form";
import { useCollectionCreateMutation } from "../mutations"; import {
useCollectionChannelListingUpdate,
useCollectionCreateMutation
} from "../mutations";
import { collectionListUrl, collectionUrl } from "../urls"; import { collectionListUrl, collectionUrl } from "../urls";
export const CollectionCreate: React.FC = () => { export const CollectionCreate: React.FC = () => {
@ -24,6 +30,31 @@ export const CollectionCreate: React.FC = () => {
const [updateMetadata] = useMetadataUpdate({}); const [updateMetadata] = useMetadataUpdate({});
const [updatePrivateMetadata] = usePrivateMetadataUpdate({}); 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({ const [createCollection, createCollectionOpts] = useCollectionCreateMutation({
onCompleted: data => { onCompleted: data => {
if (data.collectionCreate.errors.length === 0) { if (data.collectionCreate.errors.length === 0) {
@ -58,13 +89,29 @@ export const CollectionCreate: React.FC = () => {
seo: { seo: {
description: formData.seoDescription, description: formData.seoDescription,
title: formData.seoTitle 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( const handleSubmit = createMetadataCreateHandler(
@ -81,10 +128,34 @@ export const CollectionCreate: React.FC = () => {
description: "window title" 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 <CollectionCreatePage
errors={createCollectionOpts.data?.collectionCreate.errors || []} errors={createCollectionOpts.data?.collectionCreate.errors || []}
channelsErrors={
updateChannelsOpts?.data?.collectionChannelListingUpdate.errors || []
}
currentChannels={currentChannels}
channelsCount={channelsData?.channels?.length}
openChannelsModal={handleChannelsModalOpen}
onChannelsChange={setCurrentChannels}
onBack={() => navigate(collectionListUrl())} onBack={() => navigate(collectionListUrl())}
disabled={createCollectionOpts.loading} disabled={createCollectionOpts.loading || updateChannelsOpts.loading}
onSubmit={handleSubmit} onSubmit={handleSubmit}
saveButtonBarState={createCollectionOpts.status} saveButtonBarState={createCollectionOpts.status}
/> />

View file

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

View file

@ -1,35 +1,31 @@
import Button from "@material-ui/core/Button";
import DialogContentText from "@material-ui/core/DialogContentText"; import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import ChannelSettingsDialog from "@saleor/channels/components/ChannelSettingsDialog";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
import SaveFilterTabDialog, { import SaveFilterTabDialog, {
SaveFilterTabDialogFormData SaveFilterTabDialogFormData
} from "@saleor/components/SaveFilterTabDialog"; } from "@saleor/components/SaveFilterTabDialog";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useChannelsSettings from "@saleor/hooks/useChannelsSettings";
import useListSettings from "@saleor/hooks/useListSettings"; import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, { import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types"; import { ListViews } from "@saleor/types";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createSortHandler from "@saleor/utils/handlers/sortHandler";
import { getSortParams } from "@saleor/utils/sort"; import { getSortParams } from "@saleor/utils/sort";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import CollectionListPage from "../../components/CollectionListPage/CollectionListPage"; import CollectionListPage from "../../components/CollectionListPage/CollectionListPage";
import { import { useCollectionBulkDelete } from "../../mutations";
useCollectionBulkDelete,
useCollectionBulkPublish
} from "../../mutations";
import { useCollectionListQuery } from "../../queries"; import { useCollectionListQuery } from "../../queries";
import { import {
collectionAddUrl, collectionAddUrl,
@ -42,8 +38,6 @@ import {
areFiltersApplied, areFiltersApplied,
deleteFilterTab, deleteFilterTab,
getActiveFilters, getActiveFilters,
getFilterOpts,
getFilterQueryParam,
getFilterTabs, getFilterTabs,
getFilterVariables, getFilterVariables,
saveFilterTab saveFilterTab
@ -58,7 +52,6 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const paginate = usePaginator(); const paginate = usePaginator();
const shop = useShop();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids 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 tabs = getFilterTabs();
const currentTab = const currentTab =
@ -124,23 +99,27 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
: 0 : 0
: parseInt(params.activeTab, 0); : parseInt(params.activeTab, 0);
const [ const handleSearchChange = (query: string) => {
changeFilters, navigate(
resetFilters, collectionListUrl({
handleSearchChange ...getActiveFilters(params),
] = createFilterHandlers({ activeTab: undefined,
cleanupFn: reset, query
createUrl: collectionListUrl, })
getFilterQueryParam, );
navigate, };
params
});
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
CollectionListUrlDialog, CollectionListUrlDialog,
CollectionListUrlQueryParams CollectionListUrlQueryParams
>(navigate, collectionListUrl, params); >(navigate, collectionListUrl, params);
const {
channelChoices,
handleChannelSelectConfirm,
selectedChannel
} = useChannelsSettings("collectionListChannel", { closeModal, openModal });
const handleTabChange = (tab: number) => { const handleTabChange = (tab: number) => {
reset(); reset();
navigate( navigate(
@ -169,19 +148,25 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
); );
const handleSort = createSortHandler(navigate, collectionListUrl, params); const handleSort = createSortHandler(navigate, collectionListUrl, params);
const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
return ( return (
<> <>
{!!channelChoices?.length && (
<ChannelSettingsDialog
channelsChoices={channelChoices}
defaultChoice={selectedChannel}
open={params.action === "settings"}
confirmButtonState="default"
onClose={closeModal}
onConfirm={handleChannelSelectConfirm}
/>
)}
<CollectionListPage <CollectionListPage
currencySymbol={currencySymbol}
currentTab={currentTab} currentTab={currentTab}
filterOpts={getFilterOpts(params)}
initialSearch={params.query || ""} initialSearch={params.query || ""}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
onFilterChange={changeFilters}
onAdd={() => navigate(collectionAddUrl)} onAdd={() => navigate(collectionAddUrl)}
onAll={resetFilters} onAll={() => navigate(collectionListUrl())}
onTabChange={handleTabChange} onTabChange={handleTabChange}
onTabDelete={() => openModal("delete-search")} onTabDelete={() => openModal("delete-search")}
onTabSave={() => openModal("save-search")} onTabSave={() => openModal("save-search")}
@ -197,108 +182,27 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
sort={getSortParams(params)} sort={getSortParams(params)}
onRowClick={id => () => navigate(collectionUrl(id))} onRowClick={id => () => navigate(collectionUrl(id))}
toolbar={ toolbar={
<> <IconButton
<Button color="primary"
color="primary" onClick={() =>
onClick={() => openModal("remove", {
openModal("unpublish", { ids: listElements
ids: listElements })
}) }
} >
> <DeleteIcon />
<FormattedMessage </IconButton>
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>
</>
} }
isChecked={isSelected} isChecked={isSelected}
selected={listElements.length} selected={listElements.length}
toggle={toggle} toggle={toggle}
toggleAll={toggleAll} 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 <ActionDialog
open={params.action === "remove" && maybe(() => params.ids.length > 0)} open={params.action === "remove" && maybe(() => params.ids.length > 0)}
onClose={closeModal} onClose={closeModal}

View file

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

View file

@ -238,7 +238,7 @@ const useStyles = makeStyles(
padding: 25 padding: 25
}, },
popover: { popover: {
zIndex: 1 zIndex: 2
}, },
root: { root: {
width: `100%` 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 Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
const productChannels = createChannelsDataFromProduct(product(""));
import AvailabilityCard from "./AvailabilityCard"; import AvailabilityCard from "./AvailabilityCard";
const props = { const props = {
data: { allChannelsCount: 4,
availableForPurchase: "", channels: productChannels,
isAvailableForPurchase: false,
isPublished: true,
publicationDate: "",
visibleInListings: true
},
errors: [], errors: [],
messages: { messages: {
hiddenLabel: "Not published", hiddenLabel: "Not published",
hiddenSecondLabel: "hidden label", hiddenSecondLabel: "hidden label",
visibleLabel: "Published" visibleLabel: "Published"
}, },
onChange: () => undefined onChange: () => undefined,
openModal: () => undefined,
selectedChannelsCount: 3
}; };
storiesOf("Generics / AvailabilityCard", module) storiesOf("Generics / AvailabilityCard", module)

View file

@ -1,51 +1,92 @@
import VisibilityCard, { import ChannelsAvailability, {
VisibilityCardProps ChannelsAvailabilityProps,
} from "@saleor/components/VisibilityCard"; Message
} from "@saleor/components/ChannelsAvailability";
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
interface AvailabilityCardProps extends VisibilityCardProps { interface AvailabilityCardProps {
data: { messages: Message;
availableForPurchase: string;
isAvailableForPurchase: boolean;
isPublished: boolean;
publicationDate: string;
visibleInListings: boolean;
};
} }
export const AvailabilityCard: React.FC<AvailabilityCardProps> = props => { export const AvailabilityCard: React.FC<AvailabilityCardProps &
Omit<ChannelsAvailabilityProps, "channelsMessages">> = props => {
const intl = useIntl(); const intl = useIntl();
const localizeDate = useDateLocalize(); const localizeDate = useDateLocalize();
return ( return (
<VisibilityCard <ChannelsAvailability
{...props} {...props}
messages={{ channelsMessages={props.channels.reduce(
...props.messages, (prevVal, currVal) => ({
availableLabel: intl.formatMessage({ ...prevVal,
defaultMessage: "Available for purchase", [currVal.id]: {
description: "product availability" ...props.messages,
}), availableDateText:
availableSecondLabel: intl.formatMessage( currVal.publicationDate && !currVal.isPublished
{ ? intl.formatMessage(
defaultMessage: "will become available on {date}", {
description: "product available for purchase date" defaultMessage: "Will become available on {date}",
}, description: "channel publication date"
{ },
date: localizeDate(props.data.availableForPurchase, "L") {
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( const useStyles = makeStyles(
theme => ({ theme => ({
container: {
zIndex: 1
},
iconButton: { iconButton: {
background: theme.palette.background.paper, background: theme.palette.background.paper,
borderRadius: "100%", borderRadius: "100%",
@ -98,6 +101,7 @@ const CardMenu: React.FC<CardMenuProps> = props => {
</IconButton> </IconButton>
<Popper <Popper
placement="bottom-end" placement="bottom-end"
className={classes.container}
open={open} open={open}
anchorEl={anchorRef.current} anchorEl={anchorRef.current}
transition 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