Use root level channel argument (#1126)

* Use root level channel argument

* Update schema after rebase

* Adjust list views (#1187)

* Use root level channel argument

* Use channel from filters

* Handle sort keys that require channel

* Add channel filter on sales and vouchers pages

* Update queries

* Fix tests/fixtures

* Block sort on discount pages

* Add generic filter validation

* Fix tests, update changelog

* Channels availability status changes, code cleanup

* Update snapshots
This commit is contained in:
Jakub Majorek 2021-06-28 15:51:19 +02:00 committed by GitHub
parent 1b7ed5babf
commit 29f6c5496b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 1529 additions and 1698 deletions

View file

@ -55,6 +55,8 @@ All notable, unreleased changes to this project will be documented in this file.
- Fix label names in reference attributes - #1184 by @orzechdev - Fix label names in reference attributes - #1184 by @orzechdev
- Fix failing product update with file attribute - #1190 by @orzechdev - Fix failing product update with file attribute - #1190 by @orzechdev
- Fix breaking select popups in filters - #1193 by @orzechdev - Fix breaking select popups in filters - #1193 by @orzechdev
- Create channel filters in product, sales and voucher lists - #1187 by @jwm0
- Add generic filter validation - #1187 by @jwm0
# 2.11.1 # 2.11.1

View file

@ -1986,14 +1986,14 @@
"context": "product channel publication status", "context": "product channel publication status",
"string": "hidden" "string": "hidden"
}, },
"src_dot_components_dot_ChannelsAvailabilityDropdown_dot_1484966255": {
"context": "product status title",
"string": "Available in {count}/{allCount}"
},
"src_dot_components_dot_ChannelsAvailabilityDropdown_dot_1702481199": { "src_dot_components_dot_ChannelsAvailabilityDropdown_dot_1702481199": {
"context": "product channel publication date", "context": "product channel publication date",
"string": "published since {date}" "string": "published since {date}"
}, },
"src_dot_components_dot_ChannelsAvailabilityDropdown_dot_1944644572": {
"context": "product status title",
"string": "{count}/{allCount} channels"
},
"src_dot_components_dot_ChannelsAvailabilityDropdown_dot_3285520461": { "src_dot_components_dot_ChannelsAvailabilityDropdown_dot_3285520461": {
"context": "product channel publication date", "context": "product channel publication date",
"string": "Will become available on {date}" "string": "Will become available on {date}"
@ -2103,6 +2103,10 @@
"context": "button", "context": "button",
"string": "Filters" "string": "Filters"
}, },
"src_dot_components_dot_Filter_dot_DEPENDENCIES_MISSING": {
"context": "filters error messages dependencies missing",
"string": "Filter requires other filters: {dependencies}"
},
"src_dot_components_dot_Filter_dot_FilterContent_dot_2779594451": { "src_dot_components_dot_Filter_dot_FilterContent_dot_2779594451": {
"context": "filter range separator", "context": "filter range separator",
"string": "and" "string": "and"
@ -2110,6 +2114,14 @@
"src_dot_components_dot_Filter_dot_FilterContent_dot_996289613": { "src_dot_components_dot_Filter_dot_FilterContent_dot_996289613": {
"string": "Filters" "string": "Filters"
}, },
"src_dot_components_dot_Filter_dot_UNKNOWN_ERROR": {
"context": "filters error messages unknown error",
"string": "Unknown error occurred"
},
"src_dot_components_dot_Filter_dot_VALUE_REQUIRED": {
"context": "filters error messages value required",
"string": "Choose a value"
},
"src_dot_components_dot_ImageUpload_dot_1731007575": { "src_dot_components_dot_ImageUpload_dot_1731007575": {
"context": "image upload", "context": "image upload",
"string": "Drop here to upload" "string": "Drop here to upload"
@ -2893,6 +2905,10 @@
"context": "sale status", "context": "sale status",
"string": "Active" "string": "Active"
}, },
"src_dot_discounts_dot_components_dot_SaleListPage_dot_channel": {
"context": "sale channel",
"string": "Channel"
},
"src_dot_discounts_dot_components_dot_SaleListPage_dot_expired": { "src_dot_discounts_dot_components_dot_SaleListPage_dot_expired": {
"context": "sale status", "context": "sale status",
"string": "Expired" "string": "Expired"
@ -3054,6 +3070,10 @@
"context": "voucher status", "context": "voucher status",
"string": "Active" "string": "Active"
}, },
"src_dot_discounts_dot_components_dot_VoucherListPage_dot_channel": {
"context": "voucher channel",
"string": "Channel"
},
"src_dot_discounts_dot_components_dot_VoucherListPage_dot_expired": { "src_dot_discounts_dot_components_dot_VoucherListPage_dot_expired": {
"context": "voucher status", "context": "voucher status",
"string": "Expired" "string": "Expired"
@ -5007,8 +5027,8 @@
"string": "All Plugins" "string": "All Plugins"
}, },
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_active": { "src_dot_plugins_dot_components_dot_PluginsListPage_dot_active": {
"context": "plugin filters error messages active", "context": "plugin filters error messages status",
"string": "Active is not selected" "string": "Status is not selected"
}, },
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_channelStatusSectionSubtitle": { "src_dot_plugins_dot_components_dot_PluginsListPage_dot_channelStatusSectionSubtitle": {
"context": "status section subtitle", "context": "status section subtitle",
@ -5199,6 +5219,18 @@
"context": "products section name", "context": "products section name",
"string": "Products" "string": "Products"
}, },
"src_dot_products_dot_components_dot_ProductAvailabilityStatusLabel_dot_published": {
"context": "product publication date",
"string": "Published on {date}"
},
"src_dot_products_dot_components_dot_ProductAvailabilityStatusLabel_dot_unpublished": {
"context": "product publication date",
"string": "Unpublished"
},
"src_dot_products_dot_components_dot_ProductAvailabilityStatusLabel_dot_willBePublished": {
"context": "product publication date",
"string": "Becomes published on {date}"
},
"src_dot_products_dot_components_dot_ProductCategoryAndCollectionsForm_dot_1755013298": { "src_dot_products_dot_components_dot_ProductCategoryAndCollectionsForm_dot_1755013298": {
"string": "Category" "string": "Category"
}, },
@ -5364,6 +5396,10 @@
"context": "product status", "context": "product status",
"string": "Available" "string": "Available"
}, },
"src_dot_products_dot_components_dot_ProductListPage_dot_channel": {
"context": "sales channel",
"string": "Channel"
},
"src_dot_products_dot_components_dot_ProductListPage_dot_hidden": { "src_dot_products_dot_components_dot_ProductListPage_dot_hidden": {
"context": "product is hidden", "context": "product is hidden",
"string": "Hidden" "string": "Hidden"

View file

@ -506,13 +506,12 @@ input AttributeFilterInput {
filterableInStorefront: Boolean filterableInStorefront: Boolean
filterableInDashboard: Boolean filterableInDashboard: Boolean
availableInGrid: Boolean availableInGrid: Boolean
metadata: [MetadataInput] metadata: [MetadataFilter]
search: String search: String
ids: [ID] ids: [ID]
type: AttributeTypeEnum type: AttributeTypeEnum
inCollection: ID inCollection: ID
inCategory: ID inCategory: ID
channel: String
} }
input AttributeInput { input AttributeInput {
@ -777,7 +776,7 @@ type CategoryDelete {
input CategoryFilterInput { input CategoryFilterInput {
search: String search: String
metadata: [MetadataInput] metadata: [MetadataFilter]
ids: [ID] ids: [ID]
} }
@ -798,7 +797,6 @@ enum CategorySortField {
input CategorySortingInput { input CategorySortingInput {
direction: OrderDirection! direction: OrderDirection!
channel: String
field: CategorySortField! field: CategorySortField!
} }
@ -1230,9 +1228,8 @@ enum CollectionErrorCode {
input CollectionFilterInput { input CollectionFilterInput {
published: CollectionPublished published: CollectionPublished
search: String search: String
metadata: [MetadataInput] metadata: [MetadataFilter]
ids: [ID] ids: [ID]
channel: String
} }
input CollectionInput { input CollectionInput {
@ -1272,7 +1269,6 @@ enum CollectionSortField {
input CollectionSortingInput { input CollectionSortingInput {
direction: OrderDirection! direction: OrderDirection!
channel: String
field: CollectionSortField! field: CollectionSortField!
} }
@ -1670,7 +1666,7 @@ input CustomerFilterInput {
numberOfOrders: IntRangeInput numberOfOrders: IntRangeInput
placedOrders: DateRangeInput placedOrders: DateRangeInput
search: String search: String
metadata: [MetadataInput] metadata: [MetadataFilter]
} }
input CustomerInput { input CustomerInput {
@ -2523,7 +2519,7 @@ enum MenuErrorCode {
input MenuFilterInput { input MenuFilterInput {
search: String search: String
slug: [String] slug: [String]
metadata: [MetadataInput] metadata: [MetadataFilter]
} }
input MenuInput { input MenuInput {
@ -2588,7 +2584,7 @@ type MenuItemDelete {
input MenuItemFilterInput { input MenuItemFilterInput {
search: String search: String
metadata: [MetadataInput] metadata: [MetadataFilter]
} }
input MenuItemInput { input MenuItemInput {
@ -2674,6 +2670,11 @@ enum MetadataErrorCode {
REQUIRED REQUIRED
} }
input MetadataFilter {
key: String!
value: String
}
input MetadataInput { input MetadataInput {
key: String! key: String!
value: String! value: String!
@ -3132,7 +3133,7 @@ input OrderDraftFilterInput {
customer: String customer: String
created: DateRangeInput created: DateRangeInput
search: String search: String
metadata: [MetadataInput] metadata: [MetadataFilter]
channels: [ID] channels: [ID]
} }
@ -3293,7 +3294,7 @@ input OrderFilterInput {
customer: String customer: String
created: DateRangeInput created: DateRangeInput
search: String search: String
metadata: [MetadataInput] metadata: [MetadataFilter]
channels: [ID] channels: [ID]
} }
@ -3624,7 +3625,7 @@ enum PageErrorCode {
input PageFilterInput { input PageFilterInput {
search: String search: String
metadata: [MetadataInput] metadata: [MetadataFilter]
pageTypes: [ID] pageTypes: [ID]
ids: [ID] ids: [ID]
} }
@ -4284,12 +4285,11 @@ input ProductFilterInput {
stockAvailability: StockAvailability stockAvailability: StockAvailability
stocks: ProductStockFilterInput stocks: ProductStockFilterInput
search: String search: String
metadata: [MetadataInput] metadata: [MetadataFilter]
price: PriceRangeInput price: PriceRangeInput
minimalPrice: PriceRangeInput minimalPrice: PriceRangeInput
productTypes: [ID] productTypes: [ID]
ids: [ID] ids: [ID]
channel: String
} }
type ProductImage { type ProductImage {
@ -4374,7 +4374,6 @@ input ProductMediaUpdateInput {
input ProductOrder { input ProductOrder {
direction: OrderDirection! direction: OrderDirection!
channel: String
attributeId: ID attributeId: ID
field: ProductOrderField field: ProductOrderField
} }
@ -4499,7 +4498,7 @@ input ProductTypeFilterInput {
search: String search: String
configurable: ProductTypeConfigurable configurable: ProductTypeConfigurable
productType: ProductTypeEnum productType: ProductTypeEnum
metadata: [MetadataInput] metadata: [MetadataFilter]
ids: [ID] ids: [ID]
} }
@ -4644,7 +4643,7 @@ type ProductVariantDelete {
input ProductVariantFilterInput { input ProductVariantFilterInput {
search: String search: String
sku: [String] sku: [String]
metadata: [MetadataInput] metadata: [MetadataFilter]
} }
input ProductVariantInput { input ProductVariantInput {
@ -4781,7 +4780,7 @@ type Query {
checkoutLines(before: String, after: String, first: Int, last: Int): CheckoutLineCountableConnection checkoutLines(before: String, after: String, first: Int, last: Int): CheckoutLineCountableConnection
channel(id: ID): Channel channel(id: ID): Channel
channels: [Channel!] channels: [Channel!]
attributes(filter: AttributeFilterInput, sortBy: AttributeSortingInput, before: String, after: String, first: Int, last: Int): AttributeCountableConnection attributes(filter: AttributeFilterInput, sortBy: AttributeSortingInput, channel: String, before: String, after: String, first: Int, last: Int): AttributeCountableConnection
attribute(id: ID, slug: String): Attribute attribute(id: ID, slug: String): Attribute
appsInstallations: [AppInstallation!]! appsInstallations: [AppInstallation!]!
apps(filter: AppFilterInput, sortBy: AppSortingInput, before: String, after: String, first: Int, last: Int): AppCountableConnection apps(filter: AppFilterInput, sortBy: AppSortingInput, before: String, after: String, first: Int, last: Int): AppCountableConnection
@ -4938,7 +4937,6 @@ enum SaleSortField {
input SaleSortingInput { input SaleSortingInput {
direction: OrderDirection! direction: OrderDirection!
channel: String
field: SaleSortField! field: SaleSortField!
} }
@ -5885,7 +5883,6 @@ enum VoucherSortField {
input VoucherSortingInput { input VoucherSortingInput {
direction: OrderDirection! direction: OrderDirection!
channel: String
field: VoucherSortField! field: VoucherSortField!
} }

View file

@ -94,7 +94,7 @@ const CategoryList: React.FC<CategoryListProps> = props => {
} }
arrowPosition="right" arrowPosition="right"
className={classes.colName} className={classes.colName}
disableClick={!isRoot} disabled={!isRoot}
onClick={() => isRoot && onSort(CategoryListUrlSortField.name)} onClick={() => isRoot && onSort(CategoryListUrlSortField.name)}
> >
<FormattedMessage defaultMessage="Category Name" /> <FormattedMessage defaultMessage="Category Name" />
@ -106,7 +106,7 @@ const CategoryList: React.FC<CategoryListProps> = props => {
: undefined : undefined
} }
className={classes.colSubcategories} className={classes.colSubcategories}
disableClick={!isRoot} disabled={!isRoot}
onClick={() => onClick={() =>
isRoot && onSort(CategoryListUrlSortField.subcategoryCount) isRoot && onSort(CategoryListUrlSortField.subcategoryCount)
} }
@ -123,7 +123,7 @@ const CategoryList: React.FC<CategoryListProps> = props => {
: undefined : undefined
} }
className={classes.colProducts} className={classes.colProducts}
disableClick={!isRoot} disabled={!isRoot}
onClick={() => onClick={() =>
isRoot && onSort(CategoryListUrlSortField.productCount) isRoot && onSort(CategoryListUrlSortField.productCount)
} }

View file

@ -194,7 +194,6 @@ export const CategoryProductList: React.FC<CategoryProductListProps> = props =>
) : product?.channelListings !== undefined ? ( ) : product?.channelListings !== undefined ? (
<ChannelsAvailabilityDropdown <ChannelsAvailabilityDropdown
allChannelsCount={channelsCount} allChannelsCount={channelsCount}
currentChannel={channel || product?.channelListings[0]}
channels={product?.channelListings} channels={product?.channelListings}
/> />
) : ( ) : (

View file

@ -102,7 +102,6 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
<CollectionProducts <CollectionProducts
disabled={disabled} disabled={disabled}
channelsCount={channelsCount} channelsCount={channelsCount}
selectedChannelId={selectedChannelId}
collection={collection} collection={collection}
{...collectionProductsProps} {...collectionProductsProps}
/> />

View file

@ -184,8 +184,8 @@ const CollectionList: React.FC<CollectionListProps> = props => {
channel ? ( channel ? (
<ChannelsAvailabilityDropdown <ChannelsAvailabilityDropdown
allChannelsCount={channelsCount} allChannelsCount={channelsCount}
currentChannel={channel}
channels={collection?.channelListings} channels={collection?.channelListings}
showStatus
/> />
) : null ) : null
) : ( ) : (

View file

@ -23,7 +23,7 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "../../../misc";
import { ChannelProps, ListActions, PageListProps } from "../../../types"; import { ListActions, PageListProps } from "../../../types";
import { CollectionDetails_collection } from "../../types/CollectionDetails"; import { CollectionDetails_collection } from "../../types/CollectionDetails";
const useStyles = makeStyles( const useStyles = makeStyles(
@ -57,10 +57,7 @@ const useStyles = makeStyles(
{ name: "CollectionProducts" } { name: "CollectionProducts" }
); );
export interface CollectionProductsProps export interface CollectionProductsProps extends PageListProps, ListActions {
extends PageListProps,
ListActions,
ChannelProps {
collection: CollectionDetails_collection; collection: CollectionDetails_collection;
channelsCount: number; channelsCount: number;
onProductUnassign: (id: string, event: React.MouseEvent<any>) => void; onProductUnassign: (id: string, event: React.MouseEvent<any>) => void;
@ -79,7 +76,6 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
onProductUnassign, onProductUnassign,
onRowClick, onRowClick,
pageInfo, pageInfo,
selectedChannelId,
isChecked, isChecked,
selected, selected,
toggle, toggle,
@ -170,10 +166,6 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
mapEdgesToItems(collection?.products), mapEdgesToItems(collection?.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 === selectedChannelId
) || product?.channelListings[0];
return ( return (
<TableRow <TableRow
@ -209,7 +201,6 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
) : product?.channelListings !== undefined ? ( ) : product?.channelListings !== undefined ? (
<ChannelsAvailabilityDropdown <ChannelsAvailabilityDropdown
allChannelsCount={channelsCount} allChannelsCount={channelsCount}
currentChannel={channel}
channels={product?.channelListings} channels={product?.channelListings}
/> />
) : ( ) : (

View file

@ -10,7 +10,7 @@ import ChannelsAvailabilityDropdown, {
const props: ChannelsAvailabilityDropdownProps = { const props: ChannelsAvailabilityDropdownProps = {
allChannelsCount: 6, allChannelsCount: 6,
channels: productChannels, channels: productChannels,
currentChannel: productChannels[0] showStatus: true
}; };
storiesOf("Generics / ChannelsAvailabilityDropdown", module) storiesOf("Generics / ChannelsAvailabilityDropdown", module)

View file

@ -16,7 +16,7 @@ type Channels = Pick<
export interface ChannelsAvailabilityDropdownProps { export interface ChannelsAvailabilityDropdownProps {
allChannelsCount: number; allChannelsCount: number;
channels: Channels[]; channels: Channels[];
currentChannel: Channels; showStatus?: boolean;
} }
const isActive = (channelData: Channels) => channelData?.isPublished; const isActive = (channelData: Channels) => channelData?.isPublished;
@ -24,7 +24,7 @@ const isActive = (channelData: Channels) => channelData?.isPublished;
export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdownProps> = ({ export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdownProps> = ({
allChannelsCount, allChannelsCount,
channels, channels,
currentChannel showStatus = false
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const classes = useStyles({}); const classes = useStyles({});
@ -32,8 +32,12 @@ export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdown
const [anchorEl, setAnchorEl] = React.useState(null); const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = event => setAnchorEl(event.currentTarget); const handleClick = event => setAnchorEl(event.currentTarget);
const handleClose = () => setAnchorEl(null); const handleClose = () => setAnchorEl(null);
const activeInAllChannels = React.useMemo(
() => showStatus && channels.every(isActive),
[channels, showStatus]
);
return ( return (
<div onClick={e => e.stopPropagation()}> <div onClick={e => e.stopPropagation()}>
<div <div
@ -45,7 +49,7 @@ export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdown
<StatusLabel <StatusLabel
label={intl.formatMessage( label={intl.formatMessage(
{ {
defaultMessage: "Available in {count}/{allCount}", defaultMessage: "{count}/{allCount} channels",
description: "product status title" description: "product status title"
}, },
{ {
@ -53,7 +57,9 @@ export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdown
count: channels.length count: channels.length
} }
)} )}
status={isActive(currentChannel) ? "success" : "error"} status={
showStatus ? (activeInAllChannels ? "success" : "error") : undefined
}
/> />
</div> </div>
<Menu <Menu

View file

@ -12,7 +12,12 @@ import React, { useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { FilterContent } from "."; import { FilterContent } from ".";
import { FilterErrorMessages, IFilter, IFilterElement } from "./types"; import {
FilterErrorMessages,
IFilter,
IFilterElement,
InvalidFilters
} from "./types";
import useFilter from "./useFilter"; import useFilter from "./useFilter";
import { extractInvalidFilters } from "./utils"; import { extractInvalidFilters } from "./utils";
@ -103,7 +108,7 @@ const Filter: React.FC<FilterProps> = props => {
const anchor = React.useRef<HTMLDivElement>(); const anchor = React.useRef<HTMLDivElement>();
const [isFilterMenuOpened, setFilterMenuOpened] = useState(false); const [isFilterMenuOpened, setFilterMenuOpened] = useState(false);
const [filterErrors, setFilterErrors] = useState<string[]>([]); const [filterErrors, setFilterErrors] = useState<InvalidFilters<string>>({});
const [data, dispatch, reset] = useFilter(menu); const [data, dispatch, reset] = useFilter(menu);
const isFilterActive = menu.some(filterElement => filterElement.active); const isFilterActive = menu.some(filterElement => filterElement.active);
@ -111,17 +116,21 @@ const Filter: React.FC<FilterProps> = props => {
const handleSubmit = () => { const handleSubmit = () => {
const invalidFilters = extractInvalidFilters(data, menu); const invalidFilters = extractInvalidFilters(data, menu);
if (!!invalidFilters.length) { if (Object.keys(invalidFilters).length > 0) {
const parsedFilterErrors = invalidFilters.map(({ name }) => name); setFilterErrors(invalidFilters);
setFilterErrors(parsedFilterErrors);
return; return;
} }
setFilterErrors([]); setFilterErrors({});
onFilterAdd(data); onFilterAdd(data);
setFilterMenuOpened(false); setFilterMenuOpened(false);
}; };
const handleClear = () => {
reset();
setFilterErrors({});
};
return ( return (
<ClickAwayListener <ClickAwayListener
onClickAway={event => { onClickAway={event => {
@ -196,7 +205,7 @@ const Filter: React.FC<FilterProps> = props => {
dataStructure={menu} dataStructure={menu}
currencySymbol={currencySymbol} currencySymbol={currencySymbol}
filters={data} filters={data}
onClear={reset} onClear={handleClear}
onFilterPropertyChange={dispatch} onFilterPropertyChange={dispatch}
onFilterAttributeFocus={onFilterAttributeFocus} onFilterAttributeFocus={onFilterAttributeFocus}
onSubmit={handleSubmit} onSubmit={handleSubmit}

View file

@ -16,9 +16,9 @@ import { FilterReducerAction } from "../reducer";
import { import {
FieldType, FieldType,
FilterErrorMessages, FilterErrorMessages,
FilterErrors,
IFilter, IFilter,
IFilterElement IFilterElement,
InvalidFilters
} from "../types"; } from "../types";
import FilterContentBody, { FilterContentBodyProps } from "./FilterContentBody"; import FilterContentBody, { FilterContentBodyProps } from "./FilterContentBody";
import FilterContentBodyNameField from "./FilterContentBodyNameField"; import FilterContentBodyNameField from "./FilterContentBodyNameField";
@ -80,7 +80,7 @@ export interface FilterContentProps<T extends string = string> {
onSubmit: () => void; onSubmit: () => void;
currencySymbol?: string; currencySymbol?: string;
dataStructure: IFilter<T>; dataStructure: IFilter<T>;
errors?: FilterErrors; errors?: InvalidFilters<T>;
errorMessages?: FilterErrorMessages<T>; errorMessages?: FilterErrorMessages<T>;
} }
@ -204,55 +204,63 @@ const FilterContent: React.FC<FilterContentProps> = ({
<Hr /> <Hr />
{dataStructure {dataStructure
.sort((a, b) => (a.name > b.name ? 1 : -1)) .sort((a, b) => (a.name > b.name ? 1 : -1))
.map(filter => ( .map(filter => {
<ExpansionPanel const currentFilter = getFilterFromCurrentData(filter);
key={filter.name}
classes={expanderClasses} return (
data-test="channel-availability-item" <ExpansionPanel
expanded={filter.name === openedFilter?.name} key={filter.name}
> classes={expanderClasses}
<ExpansionPanelSummary data-test="channel-availability-item"
expandIcon={<IconChevronDown />} expanded={filter.name === openedFilter?.name}
classes={summaryClasses}
onClick={() => handleFilterOpen(filter)}
> >
<FilterContentBodyNameField <ExpansionPanelSummary
filter={getFilterFromCurrentData(filter)} expandIcon={<IconChevronDown />}
onFilterPropertyChange={action => classes={summaryClasses}
handleFilterPropertyGroupChange(action, filter) onClick={() => handleFilterOpen(filter)}
} >
/> <FilterContentBodyNameField
</ExpansionPanelSummary> filter={currentFilter}
<FilterErrorsList onFilterPropertyChange={action =>
errors={errors} handleFilterPropertyGroupChange(action, filter)
errorMessages={errorMessages} }
filter={filter} />
/> </ExpansionPanelSummary>
{filter.multipleFields ? ( {currentFilter.active && (
<CollectionWithDividers <FilterErrorsList
collection={filter.multipleFields} errors={errors?.[filter.name]}
renderItem={filterField => ( errorMessages={errorMessages}
<FilterContentBody filter={filter}
{...commonFilterBodyProps} />
onFilterPropertyChange={handleMultipleFieldPropertyChange} )}
filter={{ {filter.multipleFields ? (
...getFilterFromCurrentData(filterField), <CollectionWithDividers
active: getFilterFromCurrentData(filter).active collection={filter.multipleFields}
}} renderItem={filterField => (
> <FilterContentBody
<Typography>{filterField.label}</Typography> {...commonFilterBodyProps}
</FilterContentBody> onFilterPropertyChange={
)} handleMultipleFieldPropertyChange
/> }
) : ( filter={{
<FilterContentBody ...getFilterFromCurrentData(filterField),
{...commonFilterBodyProps} active: currentFilter.active
onFilterPropertyChange={onFilterPropertyChange} }}
filter={getFilterFromCurrentData(filter)} >
/> <Typography>{filterField.label}</Typography>
)} </FilterContentBody>
</ExpansionPanel> )}
))} />
) : (
<FilterContentBody
{...commonFilterBodyProps}
onFilterPropertyChange={onFilterPropertyChange}
filter={currentFilter}
/>
)}
</ExpansionPanel>
);
})}
</form> </form>
</Paper> </Paper>
); );

View file

@ -2,10 +2,12 @@ import { Typography } from "@material-ui/core";
import { fade, makeStyles } from "@material-ui/core/styles"; import { fade, makeStyles } from "@material-ui/core/styles";
import InlineAlert from "@saleor/components/Alert/InlineAlert"; import InlineAlert from "@saleor/components/Alert/InlineAlert";
import { useStyles as useDotStyles } from "@saleor/components/StatusLabel"; import { useStyles as useDotStyles } from "@saleor/components/StatusLabel";
import errorTracker from "@saleor/services/errorTracking";
import classNames from "classnames"; import classNames from "classnames";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { validationMessages } from "../messages";
import { FilterErrorMessages, FilterErrors, IFilterElement } from "../types"; import { FilterErrorMessages, FilterErrors, IFilterElement } from "../types";
const useStyles = makeStyles( const useStyles = makeStyles(
@ -36,7 +38,7 @@ interface FilterErrorsListProps<T extends string = string> {
} }
const FilterErrorsList: React.FC<FilterErrorsListProps> = ({ const FilterErrorsList: React.FC<FilterErrorsListProps> = ({
filter: { name, multipleFields }, filter: { dependencies },
errors = [], errors = [],
errorMessages errorMessages
}) => { }) => {
@ -44,18 +46,20 @@ const FilterErrorsList: React.FC<FilterErrorsListProps> = ({
const dotClasses = useDotStyles({}); const dotClasses = useDotStyles({});
const intl = useIntl(); const intl = useIntl();
const hasError = (fieldName: string) => const getErrorMessage = (code: string) => {
!!errors.find(errorName => errorName === fieldName); try {
return intl.formatMessage(
const hasErrorsToShow = () => { errorMessages?.[code] || validationMessages[code],
if (!!multipleFields?.length) { { dependencies: dependencies?.join() }
return multipleFields.some(multipleField => hasError(multipleField.name)); );
} catch (e) {
errorTracker.captureException(e);
console.warn("Translation missing for filter error code: ", code);
return intl.formatMessage(validationMessages.UNKNOWN_ERROR);
} }
return hasError(name);
}; };
if (!errors.length || !hasErrorsToShow()) { if (!errors.length) {
return null; return null;
} }
@ -63,11 +67,11 @@ const FilterErrorsList: React.FC<FilterErrorsListProps> = ({
<div className={classes.container}> <div className={classes.container}>
{!!errors.length && ( {!!errors.length && (
<InlineAlert> <InlineAlert>
{errors.map(fieldName => ( {errors.map(code => (
<div className={classes.itemContainer}> <div className={classes.itemContainer} key={code}>
<div className={classNames(classes.dot, dotClasses.dot)} /> <div className={classNames(classes.dot, dotClasses.dot)} />
<Typography className={classes.listItemTitle}> <Typography className={classes.listItemTitle}>
{intl.formatMessage(errorMessages?.[fieldName])} {getErrorMessage(code)}
</Typography> </Typography>
</div> </div>
))} ))}

View file

@ -0,0 +1,18 @@
import { defineMessages } from "react-intl";
import { ValidationErrorCode } from "./types";
export const validationMessages = defineMessages<ValidationErrorCode>({
VALUE_REQUIRED: {
defaultMessage: "Choose a value",
description: "filters error messages value required"
},
DEPENDENCIES_MISSING: {
defaultMessage: "Filter requires other filters: {dependencies}",
description: "filters error messages dependencies missing"
},
UNKNOWN_ERROR: {
defaultMessage: "Unknown error occurred",
description: "filters error messages unknown error"
}
});

View file

@ -33,6 +33,7 @@ export interface IFilterElement<T extends string = string>
required?: boolean; required?: boolean;
multipleFields?: IFilterElement[]; multipleFields?: IFilterElement[];
id?: string; id?: string;
dependencies?: string[];
} }
export interface FilterBaseFieldProps<T extends string = string> { export interface FilterBaseFieldProps<T extends string = string> {
@ -53,3 +54,11 @@ export enum FilterType {
MULTIPLE = "MULTIPLE", MULTIPLE = "MULTIPLE",
SINGULAR = "SINGULAR" SINGULAR = "SINGULAR"
} }
export enum ValidationErrorCode {
VALUE_REQUIRED = "VALUE_REQUIRED",
DEPENDENCIES_MISSING = "DEPENDENCIES_MISSING",
UNKNOWN_ERROR = "UNKNOWN_ERROR"
}
export type InvalidFilters<T extends string> = Record<T, string[]>;

View file

@ -1,6 +1,11 @@
import compact from "lodash/compact"; import compact from "lodash/compact";
import { FieldType, IFilterElement } from "./types"; import {
FieldType,
IFilterElement,
InvalidFilters,
ValidationErrorCode
} from "./types";
export const getByName = (nameToCompare: string) => (obj: { name: string }) => export const getByName = (nameToCompare: string) => (obj: { name: string }) =>
obj.name === nameToCompare; obj.name === nameToCompare;
@ -20,6 +25,8 @@ export const isFilterFieldValid = function<T extends string>(
case FieldType.boolean: case FieldType.boolean:
case FieldType.autocomplete: case FieldType.autocomplete:
return isAutocompleteFilterFieldValid(filter); return isAutocompleteFilterFieldValid(filter);
case FieldType.options:
return !!filter.value[0];
default: default:
return true; return true;
@ -27,42 +34,73 @@ export const isFilterFieldValid = function<T extends string>(
}; };
export const isFilterValid = function<T extends string>( export const isFilterValid = function<T extends string>(
resultFilters: Array<IFilterElement<T>>,
filter: IFilterElement<T> filter: IFilterElement<T>
) { ) {
const { required, active } = filter; const { required, active } = filter;
if (!required || !active) { if (!required && !active) {
return resultFilters; return true;
} }
return isFilterFieldValid(filter) return isFilterFieldValid(filter);
? resultFilters
: [...resultFilters, filter];
}; };
export const extractInvalidFilters = function<T extends string>( export const extractInvalidFilters = function<T extends string>(
filtersData: Array<IFilterElement<T>>, filtersData: Array<IFilterElement<T>>,
filtersDataStructure: Array<IFilterElement<T>> filtersDataStructure: Array<IFilterElement<T>>
) { ): InvalidFilters<T> {
return filtersDataStructure.reduce( return filtersDataStructure.reduce(
(resultFilters, { name, multipleFields }) => { (invalidFilters, { name, multipleFields, dependencies }) => {
const filter = filtersData.find(getByName(name)); const filter = filtersData.find(getByName(name));
let errors: string[] = [];
const shouldExtractChildrenFields = const shouldExtractChildrenFields =
filter.active && !!multipleFields?.length; filter.active && !!multipleFields?.length;
if (shouldExtractChildrenFields) { // if filter is inactive we skip entire validation
return multipleFields if (!filter.active) {
.map(field => { return invalidFilters;
const dataField = filtersData.find(getByName(field.name));
return { ...dataField, active: true };
})
.reduce(isFilterValid, resultFilters);
} }
return isFilterValid(resultFilters, filter); if (!isFilterValid(filter)) {
errors.push(ValidationErrorCode.VALUE_REQUIRED);
}
if (shouldExtractChildrenFields) {
const multipleFieldErrors = multipleFields
.map(field => {
const filter = filtersData.find(getByName(field.name));
return { ...filter, active: true };
})
.filter(el => !isFilterValid(el))
.map(({ name }) => name);
errors = [...errors, ...multipleFieldErrors];
}
// check if filter depends on other filters
if (dependencies?.length > 0) {
const deps = dependencies
.map(name => {
const filter = filtersData.find(getByName(name));
return { ...filter, required: true };
})
.filter(el => !isFilterValid(el));
if (deps.length > 0) {
errors.push(ValidationErrorCode.DEPENDENCIES_MISSING);
}
}
if (errors.length === 0) {
return invalidFilters;
}
return {
...invalidFilters,
[name]: errors
};
}, },
[] {} as InvalidFilters<T>
); );
}; };

View file

@ -23,9 +23,12 @@ const menuItemHeight = 46;
const maxMenuItems = 5; const maxMenuItems = 5;
const offset = 24; const offset = 24;
export interface SingleAutocompleteChoiceType { export type ChoiceValue = string;
export interface SingleAutocompleteChoiceType<
T extends ChoiceValue = ChoiceValue
> {
label: string; label: string;
value: any; value: T;
} }
export interface SingleAutocompleteActionType { export interface SingleAutocompleteActionType {
label: string; label: string;
@ -303,7 +306,7 @@ const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFie
<div className={classes.arrowContainer}> <div className={classes.arrowContainer}>
<div <div
className={classNames(classes.arrowInnerContainer, { className={classNames(classes.arrowInnerContainer, {
// Needs to be explicitely compared to false because // Needs to be explicitly compared to false because
// scrolledToBottom can be either true, false or undefined // scrolledToBottom can be either true, false or undefined
[classes.hide]: scrolledToBottom !== false [classes.hide]: scrolledToBottom !== false
})} })}

View file

@ -11,7 +11,9 @@ export const useStyles = makeStyles(
const dot = { const dot = {
borderRadius: "100%", borderRadius: "100%",
height: 8, height: 8,
width: 8 minHeight: 8,
width: 8,
minWidth: 8
}; };
return { return {
@ -56,9 +58,9 @@ export const useStyles = makeStyles(
{ name: "StatusLabel" } { name: "StatusLabel" }
); );
interface StatusLabelProps { export interface StatusLabelProps {
label: string | React.ReactNode; label: string | React.ReactNode;
status: "success" | "alert" | "neutral" | "error" | string; status: "success" | "alert" | "neutral" | "error" | undefined;
subtitle?: string; subtitle?: string;
className?: string; className?: string;
} }

View file

@ -17,7 +17,8 @@ const useStyles = makeStyles(
arrowUp: { arrowUp: {
transform: "rotate(180deg)" transform: "rotate(180deg)"
}, },
disableClick: { disabled: {
opacity: 0.7,
"&&": { "&&": {
cursor: "unset" cursor: "unset"
} }
@ -54,8 +55,8 @@ export type TableCellHeaderArrowPosition = "left" | "right";
export interface TableCellHeaderProps extends TableCellProps { export interface TableCellHeaderProps extends TableCellProps {
arrowPosition?: TableCellHeaderArrowPosition; arrowPosition?: TableCellHeaderArrowPosition;
direction?: TableCellHeaderArrowDirection; direction?: TableCellHeaderArrowDirection;
disableClick?: boolean;
textAlign?: "left" | "center" | "right"; textAlign?: "left" | "center" | "right";
disabled?: boolean;
} }
const TableCellHeader: React.FC<TableCellHeaderProps> = props => { const TableCellHeader: React.FC<TableCellHeaderProps> = props => {
@ -65,16 +66,22 @@ const TableCellHeader: React.FC<TableCellHeaderProps> = props => {
children, children,
className, className,
direction, direction,
disableClick,
textAlign, textAlign,
disabled = false,
onClick,
...rest ...rest
} = props; } = props;
return ( return (
<TableCell <TableCell
{...rest} {...rest}
onClick={e => {
if (!disabled) {
onClick(e);
}
}}
className={classNames(classes.root, className, { className={classNames(classes.root, className, {
[classes.disableClick]: disableClick [classes.disabled]: disabled
})} })}
> >
<div <div

View file

@ -23,14 +23,11 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "../../../misc";
import { ChannelProps, ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "../../../types";
import { SaleDetails_sale } from "../../types/SaleDetails"; import { SaleDetails_sale } from "../../types/SaleDetails";
import { VoucherDetails_voucher } from "../../types/VoucherDetails"; import { VoucherDetails_voucher } from "../../types/VoucherDetails";
export interface SaleProductsProps export interface SaleProductsProps extends ListProps, ListActions {
extends ListProps,
ListActions,
ChannelProps {
discount: SaleDetails_sale | VoucherDetails_voucher; discount: SaleDetails_sale | VoucherDetails_voucher;
channelsCount: number; channelsCount: number;
onProductAssign: () => void; onProductAssign: () => void;
@ -83,7 +80,6 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
onNextPage, onNextPage,
isChecked, isChecked,
selected, selected,
selectedChannelId,
toggle, toggle,
toggleAll, toggleAll,
toolbar toolbar
@ -162,10 +158,7 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
mapEdgesToItems(sale?.products), mapEdgesToItems(sale?.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 === selectedChannelId
) || product?.channelListings[0];
return ( return (
<TableRow <TableRow
hover={!!product} hover={!!product}
@ -200,7 +193,6 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
) : product?.channelListings !== undefined ? ( ) : product?.channelListings !== undefined ? (
<ChannelsAvailabilityDropdown <ChannelsAvailabilityDropdown
allChannelsCount={channelsCount} allChannelsCount={channelsCount}
currentChannel={channel}
channels={product?.channelListings} channels={product?.channelListings}
/> />
) : ( ) : (

View file

@ -274,7 +274,6 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
pageInfo={pageInfo} pageInfo={pageInfo}
discount={sale} discount={sale}
channelsCount={allChannelsCount} channelsCount={allChannelsCount}
selectedChannelId={selectedChannelId}
isChecked={isChecked} isChecked={isChecked}
selected={selected} selected={selected}
toggle={toggle} toggle={toggle}

View file

@ -9,6 +9,7 @@ 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";
import { SaleListUrlSortField } from "@saleor/discounts/urls"; import { SaleListUrlSortField } from "@saleor/discounts/urls";
import { canBeSorted } from "@saleor/discounts/views/SaleList/sort";
import { maybe, renderCollection } from "@saleor/misc"; import { maybe, renderCollection } from "@saleor/misc";
import { makeStyles } from "@saleor/theme"; import { makeStyles } from "@saleor/theme";
import { ChannelProps, ListActions, ListProps, SortPage } from "@saleor/types"; import { ChannelProps, ListActions, ListProps, SortPage } from "@saleor/types";
@ -141,6 +142,9 @@ const SaleList: React.FC<SaleListProps> = props => {
} }
textAlign="right" textAlign="right"
onClick={() => onSort(SaleListUrlSortField.value)} onClick={() => onSort(SaleListUrlSortField.value)}
disabled={
!canBeSorted(SaleListUrlSortField.value, !!selectedChannelId)
}
className={classes.colValue} className={classes.colValue}
> >
<FormattedMessage defaultMessage="Value" description="sale value" /> <FormattedMessage defaultMessage="Value" description="sale value" />

View file

@ -1,4 +1,5 @@
import { IFilter } from "@saleor/components/Filter"; import { IFilter } from "@saleor/components/Filter";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { FilterOpts, MinMax } from "@saleor/types"; import { FilterOpts, MinMax } from "@saleor/types";
import { import {
DiscountStatusEnum, DiscountStatusEnum,
@ -13,13 +14,15 @@ import { defineMessages, IntlShape } from "react-intl";
export enum SaleFilterKeys { export enum SaleFilterKeys {
saleType = "saleType", saleType = "saleType",
started = "started", started = "started",
status = "status" status = "status",
channel = "channel"
} }
export interface SaleListFilterOpts { export interface SaleListFilterOpts {
saleType: FilterOpts<DiscountValueTypeEnum>; saleType: FilterOpts<DiscountValueTypeEnum>;
started: FilterOpts<MinMax>; started: FilterOpts<MinMax>;
status: FilterOpts<DiscountStatusEnum[]>; status: FilterOpts<DiscountStatusEnum[]>;
channel: FilterOpts<string> & { choices: MultiAutocompleteChoiceType[] };
} }
const messages = defineMessages({ const messages = defineMessages({
@ -27,6 +30,10 @@ const messages = defineMessages({
defaultMessage: "Active", defaultMessage: "Active",
description: "sale status" description: "sale status"
}, },
channel: {
defaultMessage: "Channel",
description: "sale channel"
},
expired: { expired: {
defaultMessage: "Expired", defaultMessage: "Expired",
description: "sale status" description: "sale status"
@ -61,6 +68,16 @@ export function createFilterStructure(
opts: SaleListFilterOpts opts: SaleListFilterOpts
): IFilter<SaleFilterKeys> { ): IFilter<SaleFilterKeys> {
return [ return [
{
...createOptionsField(
SaleFilterKeys.channel,
intl.formatMessage(messages.channel),
[opts.channel.value],
false,
opts.channel.choices
),
active: opts.channel.active
},
{ {
...createDateField( ...createDateField(
SaleFilterKeys.started, SaleFilterKeys.started,

View file

@ -346,7 +346,6 @@ const VoucherDetailsPage: React.FC<VoucherDetailsPageProps> = ({
onRowClick={onProductClick} onRowClick={onProductClick}
pageInfo={pageInfo} pageInfo={pageInfo}
discount={voucher} discount={voucher}
selectedChannelId={selectedChannelId}
channelsCount={allChannelsCount} channelsCount={allChannelsCount}
isChecked={isChecked} isChecked={isChecked}
selected={selected} selected={selected}

View file

@ -9,6 +9,7 @@ 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";
import { VoucherListUrlSortField } from "@saleor/discounts/urls"; import { VoucherListUrlSortField } from "@saleor/discounts/urls";
import { canBeSorted } from "@saleor/discounts/views/VoucherList/sort";
import { maybe, renderCollection } from "@saleor/misc"; import { maybe, renderCollection } from "@saleor/misc";
import { makeStyles } from "@saleor/theme"; import { makeStyles } from "@saleor/theme";
import { ChannelProps, ListActions, ListProps, SortPage } from "@saleor/types"; import { ChannelProps, ListActions, ListProps, SortPage } from "@saleor/types";
@ -130,6 +131,9 @@ const VoucherList: React.FC<VoucherListProps> = props => {
} }
textAlign="right" textAlign="right"
onClick={() => onSort(VoucherListUrlSortField.minSpent)} onClick={() => onSort(VoucherListUrlSortField.minSpent)}
disabled={
!canBeSorted(VoucherListUrlSortField.minSpent, !!selectedChannelId)
}
className={classes.colMinSpent} className={classes.colMinSpent}
> >
<FormattedMessage <FormattedMessage
@ -175,6 +179,9 @@ const VoucherList: React.FC<VoucherListProps> = props => {
} }
textAlign="right" textAlign="right"
onClick={() => onSort(VoucherListUrlSortField.value)} onClick={() => onSort(VoucherListUrlSortField.value)}
disabled={
!canBeSorted(VoucherListUrlSortField.minSpent, !!selectedChannelId)
}
className={classes.colValue} className={classes.colValue}
> >
<FormattedMessage <FormattedMessage

View file

@ -1,4 +1,5 @@
import { IFilter } from "@saleor/components/Filter"; import { IFilter } from "@saleor/components/Filter";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { FilterOpts, MinMax } from "@saleor/types"; import { FilterOpts, MinMax } from "@saleor/types";
import { import {
DiscountStatusEnum, DiscountStatusEnum,
@ -15,7 +16,8 @@ export enum VoucherFilterKeys {
saleType = "saleType", saleType = "saleType",
started = "started", started = "started",
status = "status", status = "status",
timesUsed = "timesUsed" timesUsed = "timesUsed",
channel = "channel"
} }
export interface VoucherListFilterOpts { export interface VoucherListFilterOpts {
@ -23,6 +25,7 @@ export interface VoucherListFilterOpts {
started: FilterOpts<MinMax>; started: FilterOpts<MinMax>;
status: FilterOpts<DiscountStatusEnum[]>; status: FilterOpts<DiscountStatusEnum[]>;
timesUsed: FilterOpts<MinMax>; timesUsed: FilterOpts<MinMax>;
channel: FilterOpts<string> & { choices: MultiAutocompleteChoiceType[] };
} }
const messages = defineMessages({ const messages = defineMessages({
@ -30,6 +33,10 @@ const messages = defineMessages({
defaultMessage: "Active", defaultMessage: "Active",
description: "voucher status" description: "voucher status"
}, },
channel: {
defaultMessage: "Channel",
description: "voucher channel"
},
expired: { expired: {
defaultMessage: "Expired", defaultMessage: "Expired",
description: "voucher status" description: "voucher status"
@ -68,6 +75,16 @@ export function createFilterStructure(
opts: VoucherListFilterOpts opts: VoucherListFilterOpts
): IFilter<VoucherFilterKeys> { ): IFilter<VoucherFilterKeys> {
return [ return [
{
...createOptionsField(
VoucherFilterKeys.channel,
intl.formatMessage(messages.channel),
[opts.channel.value],
false,
opts.channel.choices
),
active: opts.channel.active
},
{ {
...createDateField( ...createDateField(
VoucherFilterKeys.started, VoucherFilterKeys.started,

View file

@ -26,6 +26,7 @@ export const saleList = gql`
$last: Int $last: Int
$filter: SaleFilterInput $filter: SaleFilterInput
$sort: SaleSortingInput $sort: SaleSortingInput
$channel: String
) { ) {
sales( sales(
after: $after after: $after
@ -34,6 +35,7 @@ export const saleList = gql`
last: $last last: $last
filter: $filter filter: $filter
sortBy: $sort sortBy: $sort
channel: $channel
) { ) {
edges { edges {
node { node {
@ -60,6 +62,7 @@ export const voucherList = gql`
$last: Int $last: Int
$filter: VoucherFilterInput $filter: VoucherFilterInput
$sort: VoucherSortingInput $sort: VoucherSortingInput
$channel: String
) { ) {
vouchers( vouchers(
after: $after after: $after
@ -68,6 +71,7 @@ export const voucherList = gql`
last: $last last: $last
filter: $filter filter: $filter
sortBy: $sort sortBy: $sort
channel: $channel
) { ) {
edges { edges {
node { node {

View file

@ -64,4 +64,5 @@ export interface SaleListVariables {
last?: number | null; last?: number | null;
filter?: SaleFilterInput | null; filter?: SaleFilterInput | null;
sort?: SaleSortingInput | null; sort?: SaleSortingInput | null;
channel?: string | null;
} }

View file

@ -81,4 +81,5 @@ export interface VoucherListVariables {
last?: number | null; last?: number | null;
filter?: VoucherFilterInput | null; filter?: VoucherFilterInput | null;
sort?: VoucherSortingInput | null; sort?: VoucherSortingInput | null;
channel?: string | null;
} }

View file

@ -23,7 +23,8 @@ export enum SaleListUrlFiltersEnum {
type = "type", type = "type",
startedFrom = "startedFrom", startedFrom = "startedFrom",
startedTo = "startedTo", startedTo = "startedTo",
query = "query" query = "query",
channel = "channel"
} }
export enum SaleListUrlFiltersWithMultipleValues { export enum SaleListUrlFiltersWithMultipleValues {
status = "status" status = "status"
@ -75,7 +76,8 @@ export enum VoucherListUrlFiltersEnum {
startedTo = "startedTo", startedTo = "startedTo",
timesUsedFrom = "timesUsedFrom", timesUsedFrom = "timesUsedFrom",
timesUsedTo = "timesUsedTo", timesUsedTo = "timesUsedTo",
query = "query" query = "query",
channel = "channel"
} }
export enum VoucherListUrlFiltersWithMultipleValues { export enum VoucherListUrlFiltersWithMultipleValues {
status = "status", status = "status",

View file

@ -20,9 +20,9 @@ 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 createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createSortHandler from "@saleor/utils/handlers/sortHandler";
import { mapEdgesToItems } from "@saleor/utils/maps"; import { mapEdgesToItems, mapNodeToChoice } from "@saleor/utils/maps";
import { getSortParams } from "@saleor/utils/sort"; import { getSortParams } from "@saleor/utils/sort";
import React from "react"; import React, { useEffect } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import SaleListPage from "../../components/SaleListPage"; import SaleListPage from "../../components/SaleListPage";
@ -46,7 +46,7 @@ import {
getFilterVariables, getFilterVariables,
saveFilterTab saveFilterTab
} from "./filters"; } from "./filters";
import { getSortQueryVariables } from "./sort"; import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
interface SaleListProps { interface SaleListProps {
params: SaleListUrlQueryParams; params: SaleListUrlQueryParams;
@ -63,7 +63,13 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
ListViews.SALES_LIST ListViews.SALES_LIST
); );
const intl = useIntl(); const intl = useIntl();
const { channel } = useAppChannel(); const { availableChannels } = useAppChannel(false);
const selectedChannel = availableChannels.find(
channel => channel.slug === params.channel
);
const channelOpts = availableChannels
? mapNodeToChoice(availableChannels, channel => channel.slug)
: null;
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
SaleListUrlDialog, SaleListUrlDialog,
@ -75,7 +81,8 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
() => ({ () => ({
...paginationState, ...paginationState,
filter: getFilterVariables(params), filter: getFilterVariables(params),
sort: getSortQueryVariables(params, channel?.slug) sort: getSortQueryVariables(params),
channel: params.channel
}), }),
[params] [params]
); );
@ -105,6 +112,17 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
params params
}); });
useEffect(() => {
if (!canBeSorted(params.sort, !!selectedChannel)) {
navigate(
saleListUrl({
...params,
sort: DEFAULT_SORT_KEY
})
);
}
}, [params]);
const handleTabChange = (tab: number) => { const handleTabChange = (tab: number) => {
reset(); reset();
navigate( navigate(
@ -163,7 +181,7 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
<WindowTitle title={intl.formatMessage(sectionNames.sales)} /> <WindowTitle title={intl.formatMessage(sectionNames.sales)} />
<SaleListPage <SaleListPage
currentTab={currentTab} currentTab={currentTab}
filterOpts={getFilterOpts(params)} filterOpts={getFilterOpts(params, channelOpts)}
initialSearch={params.query || ""} initialSearch={params.query || ""}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
onFilterChange={filter => changeFilters(filter)} onFilterChange={filter => changeFilters(filter)}
@ -199,7 +217,7 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
} }
selectedChannelId={channel?.id} selectedChannelId={selectedChannel?.id}
/> />
<ActionDialog <ActionDialog
confirmButtonState={saleBulkDeleteOpts.status} confirmButtonState={saleBulkDeleteOpts.status}

View file

@ -2,6 +2,7 @@
exports[`Filtering URL params should not be empty if active filters are present 1`] = ` exports[`Filtering URL params should not be empty if active filters are present 1`] = `
Object { Object {
"channel": "default-channel",
"startedFrom": "2019-12-09", "startedFrom": "2019-12-09",
"startedTo": "2019-12-38", "startedTo": "2019-12-38",
"status": Array [ "status": Array [
@ -12,4 +13,4 @@ Object {
} }
`; `;
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"startedFrom=2019-12-09&startedTo=2019-12-38&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type=FIXED"`; exports[`Filtering URL params should not be empty if active filters are present 2`] = `"channel=default-channel&startedFrom=2019-12-09&startedTo=2019-12-38&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type=FIXED"`;

View file

@ -38,6 +38,16 @@ describe("Filtering URL params", () => {
const intl = createIntl(config); const intl = createIntl(config);
const filters = createFilterStructure(intl, { const filters = createFilterStructure(intl, {
channel: {
active: false,
choices: [
{
value: "default-channel",
label: "Default channel"
}
],
value: "default-channel"
},
saleType: { saleType: {
active: false, active: false,
value: DiscountValueTypeEnum.FIXED value: DiscountValueTypeEnum.FIXED

View file

@ -1,4 +1,5 @@
import { IFilterElement } from "@saleor/components/Filter"; import { IFilterElement } from "@saleor/components/Filter";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { import {
SaleFilterKeys, SaleFilterKeys,
SaleListFilterOpts SaleListFilterOpts
@ -17,7 +18,8 @@ import {
getGteLteVariables, getGteLteVariables,
getMinMaxQueryParam, getMinMaxQueryParam,
getMultipleEnumValueQueryParam, getMultipleEnumValueQueryParam,
getSingleEnumValueQueryParam getSingleEnumValueQueryParam,
getSingleValueQueryParam
} from "../../../utils/filters"; } from "../../../utils/filters";
import { import {
SaleListUrlFilters, SaleListUrlFilters,
@ -28,8 +30,16 @@ import {
export const SALE_FILTERS_KEY = "saleFilters"; export const SALE_FILTERS_KEY = "saleFilters";
export function getFilterOpts(params: SaleListUrlFilters): SaleListFilterOpts { export function getFilterOpts(
params: SaleListUrlFilters,
channels: SingleAutocompleteChoiceType[]
): SaleListFilterOpts {
return { return {
channel: {
active: params?.channel !== undefined,
choices: channels,
value: params?.channel
},
saleType: { saleType: {
active: !!maybe(() => params.type), active: !!maybe(() => params.type),
value: maybe(() => findValueInEnum(params.type, DiscountValueTypeEnum)) value: maybe(() => findValueInEnum(params.type, DiscountValueTypeEnum))
@ -105,6 +115,9 @@ export function getFilterQueryParam(
SaleListUrlFiltersWithMultipleValues.status, SaleListUrlFiltersWithMultipleValues.status,
DiscountStatusEnum DiscountStatusEnum
); );
case SaleFilterKeys.channel:
return getSingleValueQueryParam(filter, SaleListUrlFiltersEnum.channel);
} }
} }

View file

@ -2,6 +2,25 @@ import { SaleListUrlSortField } from "@saleor/discounts/urls";
import { SaleSortField } from "@saleor/types/globalTypes"; import { SaleSortField } from "@saleor/types/globalTypes";
import { createGetSortQueryVariables } from "@saleor/utils/sort"; import { createGetSortQueryVariables } from "@saleor/utils/sort";
export const DEFAULT_SORT_KEY = SaleListUrlSortField.name;
export function canBeSorted(
sort: SaleListUrlSortField,
isChannelSelected: boolean
) {
switch (sort) {
case SaleListUrlSortField.name:
case SaleListUrlSortField.startDate:
case SaleListUrlSortField.endDate:
case SaleListUrlSortField.type:
return true;
case SaleListUrlSortField.value:
return isChannelSelected;
default:
return false;
}
}
export function getSortQueryField(sort: SaleListUrlSortField): SaleSortField { export function getSortQueryField(sort: SaleListUrlSortField): SaleSortField {
switch (sort) { switch (sort) {
case SaleListUrlSortField.name: case SaleListUrlSortField.name:

View file

@ -20,9 +20,9 @@ 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 createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import createSortHandler from "@saleor/utils/handlers/sortHandler"; import createSortHandler from "@saleor/utils/handlers/sortHandler";
import { mapEdgesToItems } from "@saleor/utils/maps"; import { mapEdgesToItems, mapNodeToChoice } from "@saleor/utils/maps";
import { getSortParams } from "@saleor/utils/sort"; import { getSortParams } from "@saleor/utils/sort";
import React from "react"; import React, { useEffect } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import VoucherListPage from "../../components/VoucherListPage"; import VoucherListPage from "../../components/VoucherListPage";
@ -46,7 +46,7 @@ import {
getFilterVariables, getFilterVariables,
saveFilterTab saveFilterTab
} from "./filters"; } from "./filters";
import { getSortQueryVariables } from "./sort"; import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
interface VoucherListProps { interface VoucherListProps {
params: VoucherListUrlQueryParams; params: VoucherListUrlQueryParams;
@ -64,7 +64,13 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
); );
const intl = useIntl(); const intl = useIntl();
const { channel } = useAppChannel(); const { availableChannels } = useAppChannel(false);
const selectedChannel = availableChannels.find(
channel => channel.slug === params.channel
);
const channelOpts = availableChannels
? mapNodeToChoice(availableChannels, channel => channel.slug)
: null;
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
VoucherListUrlDialog, VoucherListUrlDialog,
@ -76,7 +82,8 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
() => ({ () => ({
...paginationState, ...paginationState,
filter: getFilterVariables(params), filter: getFilterVariables(params),
sort: getSortQueryVariables(params, channel?.slug) sort: getSortQueryVariables(params),
channel: params.channel
}), }),
[params] [params]
); );
@ -106,6 +113,17 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
params params
}); });
useEffect(() => {
if (!canBeSorted(params.sort, !!selectedChannel)) {
navigate(
voucherListUrl({
...params,
sort: DEFAULT_SORT_KEY
})
);
}
}, [params]);
const handleTabChange = (tab: number) => { const handleTabChange = (tab: number) => {
reset(); reset();
navigate( navigate(
@ -164,7 +182,7 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
<WindowTitle title={intl.formatMessage(sectionNames.vouchers)} /> <WindowTitle title={intl.formatMessage(sectionNames.vouchers)} />
<VoucherListPage <VoucherListPage
currentTab={currentTab} currentTab={currentTab}
filterOpts={getFilterOpts(params)} filterOpts={getFilterOpts(params, channelOpts)}
initialSearch={params.query || ""} initialSearch={params.query || ""}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
onFilterChange={filter => changeFilters(filter)} onFilterChange={filter => changeFilters(filter)}
@ -200,7 +218,7 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
} }
selectedChannelId={channel?.id} selectedChannelId={selectedChannel?.id}
/> />
<ActionDialog <ActionDialog
confirmButtonState={voucherBulkDeleteOpts.status} confirmButtonState={voucherBulkDeleteOpts.status}

View file

@ -2,6 +2,7 @@
exports[`Filtering URL params should not be empty if active filters are present 1`] = ` exports[`Filtering URL params should not be empty if active filters are present 1`] = `
Object { Object {
"channel": "default-channel",
"startedFrom": "2019-12-09", "startedFrom": "2019-12-09",
"startedTo": "2019-12-38", "startedTo": "2019-12-38",
"status": Array [ "status": Array [
@ -17,4 +18,4 @@ Object {
} }
`; `;
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"startedFrom=2019-12-09&startedTo=2019-12-38&timesUsedFrom=1&timesUsedTo=6&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type%5B0%5D=FIXED&type%5B1%5D=SHIPPING"`; exports[`Filtering URL params should not be empty if active filters are present 2`] = `"channel=default-channel&startedFrom=2019-12-09&startedTo=2019-12-38&timesUsedFrom=1&timesUsedTo=6&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type%5B0%5D=FIXED&type%5B1%5D=SHIPPING"`;

View file

@ -40,6 +40,16 @@ describe("Filtering URL params", () => {
const intl = createIntl(config); const intl = createIntl(config);
const filters = createFilterStructure(intl, { const filters = createFilterStructure(intl, {
channel: {
active: false,
choices: [
{
value: "default-channel",
label: "Default channel"
}
],
value: "default-channel"
},
saleType: { saleType: {
active: false, active: false,
value: [VoucherDiscountType.FIXED, VoucherDiscountType.SHIPPING] value: [VoucherDiscountType.FIXED, VoucherDiscountType.SHIPPING]

View file

@ -1,4 +1,5 @@
import { IFilterElement } from "@saleor/components/Filter"; import { IFilterElement } from "@saleor/components/Filter";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { import {
VoucherFilterKeys, VoucherFilterKeys,
VoucherListFilterOpts VoucherListFilterOpts
@ -16,7 +17,8 @@ import {
dedupeFilter, dedupeFilter,
getGteLteVariables, getGteLteVariables,
getMinMaxQueryParam, getMinMaxQueryParam,
getMultipleEnumValueQueryParam getMultipleEnumValueQueryParam,
getSingleValueQueryParam
} from "../../../utils/filters"; } from "../../../utils/filters";
import { import {
VoucherListUrlFilters, VoucherListUrlFilters,
@ -28,9 +30,15 @@ import {
export const VOUCHER_FILTERS_KEY = "voucherFilters"; export const VOUCHER_FILTERS_KEY = "voucherFilters";
export function getFilterOpts( export function getFilterOpts(
params: VoucherListUrlFilters params: VoucherListUrlFilters,
channels: SingleAutocompleteChoiceType[]
): VoucherListFilterOpts { ): VoucherListFilterOpts {
return { return {
channel: {
active: params?.channel !== undefined,
choices: channels,
value: params?.channel
},
saleType: { saleType: {
active: !!maybe(() => params.type), active: !!maybe(() => params.type),
value: maybe( value: maybe(
@ -137,6 +145,12 @@ export function getFilterQueryParam(
VoucherListUrlFiltersWithMultipleValues.status, VoucherListUrlFiltersWithMultipleValues.status,
DiscountStatusEnum DiscountStatusEnum
); );
case VoucherFilterKeys.channel:
return getSingleValueQueryParam(
filter,
VoucherListUrlFiltersEnum.channel
);
} }
} }

View file

@ -2,6 +2,27 @@ import { VoucherListUrlSortField } from "@saleor/discounts/urls";
import { VoucherSortField } from "@saleor/types/globalTypes"; import { VoucherSortField } from "@saleor/types/globalTypes";
import { createGetSortQueryVariables } from "@saleor/utils/sort"; import { createGetSortQueryVariables } from "@saleor/utils/sort";
export const DEFAULT_SORT_KEY = VoucherListUrlSortField.code;
export function canBeSorted(
sort: VoucherListUrlSortField,
isChannelSelected: boolean
) {
switch (sort) {
case VoucherListUrlSortField.code:
case VoucherListUrlSortField.startDate:
case VoucherListUrlSortField.endDate:
case VoucherListUrlSortField.type:
case VoucherListUrlSortField.limit:
return true;
case VoucherListUrlSortField.value:
case VoucherListUrlSortField.minSpent:
return isChannelSelected;
default:
return false;
}
}
export function getSortQueryField( export function getSortQueryField(
sort: VoucherListUrlSortField sort: VoucherListUrlSortField
): VoucherSortField { ): VoucherSortField {

View file

@ -34,7 +34,8 @@ const home = gql`
totalCount totalCount
} }
productsOutOfStock: products( productsOutOfStock: products(
filter: { stockAvailability: OUT_OF_STOCK, channel: $channel } filter: { stockAvailability: OUT_OF_STOCK }
channel: $channel
) { ) {
totalCount totalCount
} }

View file

@ -5,6 +5,7 @@ import urlJoin from "url-join";
import { ConfirmButtonTransitionState } from "./components/ConfirmButton/ConfirmButton"; import { ConfirmButtonTransitionState } from "./components/ConfirmButton/ConfirmButton";
import { StatusType } from "./components/StatusChip/types"; import { StatusType } from "./components/StatusChip/types";
import { StatusLabelProps } from "./components/StatusLabel";
import { APP_MOUNT_URI } from "./config"; import { APP_MOUNT_URI } from "./config";
import { AddressType, AddressTypeInput } from "./customers/types"; import { AddressType, AddressTypeInput } from "./customers/types";
import { import {
@ -90,7 +91,10 @@ const paymentStatusMessages = defineMessages({
} }
}); });
export const transformPaymentStatus = (status: string, intl: IntlShape) => { export const transformPaymentStatus = (
status: string,
intl: IntlShape
): { localized: string; status: StatusLabelProps["status"] } => {
switch (status) { switch (status) {
case PaymentChargeStatusEnum.PARTIALLY_CHARGED: case PaymentChargeStatusEnum.PARTIALLY_CHARGED:
return { return {

View file

@ -2,8 +2,8 @@ import { defineMessages } from "react-intl";
export const pluginsFilterErrorMessages = defineMessages({ export const pluginsFilterErrorMessages = defineMessages({
active: { active: {
defaultMessage: "Active is not selected", defaultMessage: "Status is not selected",
description: "plugin filters error messages active" description: "plugin filters error messages status"
}, },
channels: { channels: {
defaultMessage: "No channels selected", defaultMessage: "No channels selected",

View file

@ -0,0 +1,35 @@
import StatusLabel from "@saleor/components/StatusLabel";
import useDateLocalize from "@saleor/hooks/useDateLocalize";
import React from "react";
import { useIntl } from "react-intl";
import { messages } from "./messages";
export const ProductAvailabilityStatusLabel = ({ channel }) => {
const intl = useIntl();
const localizeDate = useDateLocalize();
return (
<StatusLabel
label={intl.formatMessage(
channel.publicationDate
? channel.isPublished
? messages.published
: messages.willBePublished
: messages.unpublished,
{
date: localizeDate(channel.publicationDate, "L")
}
)}
status={
channel.publicationDate
? channel.isPublished
? "success"
: "alert"
: "error"
}
/>
);
};
export default ProductAvailabilityStatusLabel;

View file

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

View file

@ -0,0 +1,16 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
published: {
defaultMessage: "Published on {date}",
description: "product publication date"
},
unpublished: {
defaultMessage: "Unpublished",
description: "product publication date"
},
willBePublished: {
defaultMessage: "Becomes published on {date}",
description: "product publication date"
}
});

View file

@ -24,6 +24,7 @@ import {
import { GridAttributes_grid_edges_node } from "@saleor/products/types/GridAttributes"; import { GridAttributes_grid_edges_node } from "@saleor/products/types/GridAttributes";
import { ProductList_products_edges_node } from "@saleor/products/types/ProductList"; import { ProductList_products_edges_node } from "@saleor/products/types/ProductList";
import { ProductListUrlSortField } from "@saleor/products/urls"; import { ProductListUrlSortField } from "@saleor/products/urls";
import { canBeSorted } from "@saleor/products/views/ProductList/sort";
import { makeStyles } from "@saleor/theme"; import { makeStyles } from "@saleor/theme";
import { ChannelProps, ListActions, ListProps, SortPage } from "@saleor/types"; import { ChannelProps, ListActions, ListProps, SortPage } from "@saleor/types";
import TDisplayColumn, { import TDisplayColumn, {
@ -34,6 +35,8 @@ import classNames from "classnames";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import ProductAvailabilityStatusLabel from "../ProductAvailabilityStatusLabel";
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
[theme.breakpoints.up("lg")]: { [theme.breakpoints.up("lg")]: {
@ -215,6 +218,12 @@ export const ProductList: React.FC<ProductListProps> = props => {
: undefined : undefined
} }
onClick={() => onSort(ProductListUrlSortField.status)} onClick={() => onSort(ProductListUrlSortField.status)}
disabled={
!canBeSorted(
ProductListUrlSortField.status,
!!selectedChannelId
)
}
> >
<FormattedMessage <FormattedMessage
defaultMessage="Availability" defaultMessage="Availability"
@ -262,6 +271,9 @@ export const ProductList: React.FC<ProductListProps> = props => {
} }
textAlign="right" textAlign="right"
onClick={() => onSort(ProductListUrlSortField.price)} onClick={() => onSort(ProductListUrlSortField.price)}
disabled={
!canBeSorted(ProductListUrlSortField.price, !!selectedChannelId)
}
> >
<FormattedMessage <FormattedMessage
defaultMessage="Price" defaultMessage="Price"
@ -361,17 +373,17 @@ export const ProductList: React.FC<ProductListProps> = props => {
!!product?.channelListings?.length !!product?.channelListings?.length
} }
> >
{product && !product?.channelListings?.length ? ( {(!product && <Skeleton />) ||
"-" (!product?.channelListings?.length && "-") ||
) : product?.channelListings !== undefined ? ( (product?.channelListings !== undefined && channel ? (
<ChannelsAvailabilityDropdown <ProductAvailabilityStatusLabel channel={channel} />
allChannelsCount={channelsCount} ) : (
currentChannel={channel} <ChannelsAvailabilityDropdown
channels={product?.channelListings} allChannelsCount={channelsCount}
/> channels={product?.channelListings}
) : ( showStatus
<Skeleton /> />
)} ))}
</TableCell> </TableCell>
</DisplayColumn> </DisplayColumn>
{gridAttributesFromSettings.map(gridAttribute => ( {gridAttributesFromSettings.map(gridAttribute => (

View file

@ -1,4 +1,5 @@
import { IFilter } from "@saleor/components/Filter"; import { IFilter } from "@saleor/components/Filter";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { commonMessages, sectionNames } from "@saleor/intl"; import { commonMessages, sectionNames } from "@saleor/intl";
import { AutocompleteFilterOpts, FilterOpts, MinMax } from "@saleor/types"; import { AutocompleteFilterOpts, FilterOpts, MinMax } from "@saleor/types";
import { import {
@ -19,7 +20,8 @@ export enum ProductFilterKeys {
collections = "collections", collections = "collections",
price = "price", price = "price",
productType = "productType", productType = "productType",
stock = "stock" stock = "stock",
channel = "channel"
} }
export interface ProductListFilterOpts { export interface ProductListFilterOpts {
@ -37,6 +39,7 @@ export interface ProductListFilterOpts {
price: FilterOpts<MinMax>; price: FilterOpts<MinMax>;
productType: FilterOpts<string[]> & AutocompleteFilterOpts; productType: FilterOpts<string[]> & AutocompleteFilterOpts;
stockStatus: FilterOpts<StockAvailability>; stockStatus: FilterOpts<StockAvailability>;
channel: FilterOpts<string> & { choices: SingleAutocompleteChoiceType[] };
} }
const messages = defineMessages({ const messages = defineMessages({
@ -44,6 +47,10 @@ const messages = defineMessages({
defaultMessage: "Available", defaultMessage: "Available",
description: "product status" description: "product status"
}, },
channel: {
defaultMessage: "Channel",
description: "sales channel"
},
hidden: { hidden: {
defaultMessage: "Hidden", defaultMessage: "Hidden",
description: "product is hidden" description: "product is hidden"
@ -81,6 +88,16 @@ export function createFilterStructure(
); );
return [ return [
{
...createOptionsField(
ProductFilterKeys.channel,
intl.formatMessage(messages.channel),
[opts.channel.value],
false,
opts.channel.choices
),
active: opts.channel.active
},
{ {
...createOptionsField( ...createOptionsField(
ProductFilterKeys.stock, ProductFilterKeys.stock,
@ -98,7 +115,8 @@ export function createFilterStructure(
} }
] ]
), ),
active: opts.stockStatus.active active: opts.stockStatus.active,
dependencies: [ProductFilterKeys.channel]
}, },
{ {
...createPriceField( ...createPriceField(

View file

@ -139,6 +139,7 @@ const productListQuery = gql`
$last: Int $last: Int
$before: String $before: String
$filter: ProductFilterInput $filter: ProductFilterInput
$channel: String
$sort: ProductOrder $sort: ProductOrder
) { ) {
products( products(
@ -148,6 +149,7 @@ const productListQuery = gql`
last: $last last: $last
filter: $filter filter: $filter
sortBy: $sort sortBy: $sort
channel: $channel
) { ) {
edges { edges {
node { node {
@ -177,8 +179,8 @@ export const useProductListQuery = makeQuery<ProductList, ProductListVariables>(
); );
const productCountQuery = gql` const productCountQuery = gql`
query ProductCount($filter: ProductFilterInput) { query ProductCount($filter: ProductFilterInput, $channel: String) {
products(filter: $filter) { products(filter: $filter, channel: $channel) {
totalCount totalCount
} }
} }

View file

@ -20,4 +20,5 @@ export interface ProductCount {
export interface ProductCountVariables { export interface ProductCountVariables {
filter?: ProductFilterInput | null; filter?: ProductFilterInput | null;
channel?: string | null;
} }

View file

@ -140,5 +140,6 @@ export interface ProductListVariables {
last?: number | null; last?: number | null;
before?: string | null; before?: string | null;
filter?: ProductFilterInput | null; filter?: ProductFilterInput | null;
channel?: string | null;
sort?: ProductOrder | null; sort?: ProductOrder | null;
} }

View file

@ -28,7 +28,8 @@ export enum ProductListUrlFiltersEnum {
priceTo = "priceTo", priceTo = "priceTo",
status = "status", status = "status",
stockStatus = "stockStatus", stockStatus = "stockStatus",
query = "query" query = "query",
channel = "channel"
} }
export enum ProductListUrlFiltersWithMultipleValues { export enum ProductListUrlFiltersWithMultipleValues {
categories = "categories", categories = "categories",

View file

@ -55,7 +55,7 @@ import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
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 createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
import { mapEdgesToItems } from "@saleor/utils/maps"; import { mapEdgesToItems, mapNodeToChoice } from "@saleor/utils/maps";
import { getSortUrlVariables } from "@saleor/utils/sort"; import { getSortUrlVariables } from "@saleor/utils/sort";
import { useWarehouseList } from "@saleor/warehouses/queries"; import { useWarehouseList } from "@saleor/warehouses/queries";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
@ -76,7 +76,7 @@ import {
getFilterVariables, getFilterVariables,
saveFilterTab saveFilterTab
} from "./filters"; } from "./filters";
import { getSortQueryVariables } from "./sort"; import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
interface ProductListProps { interface ProductListProps {
params: ProductListUrlQueryParams; params: ProductListUrlQueryParams;
@ -160,14 +160,16 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
}, },
skip: params.action !== "export" skip: params.action !== "export"
}); });
const { availableChannels, channel } = useAppChannel(); const { availableChannels } = useAppChannel(false);
const limitOpts = useShopLimitsQuery({ const limitOpts = useShopLimitsQuery({
variables: { variables: {
productVariants: true productVariants: true
} }
}); });
const noChannel = !channel && typeof channel !== "undefined"; const selectedChannel = availableChannels.find(
channel => channel.slug === params.channel
);
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
ProductListUrlDialog, ProductListUrlDialog,
@ -238,7 +240,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
const sortWithQuery = ProductListUrlSortField.rank; const sortWithQuery = ProductListUrlSortField.rank;
const sortWithoutQuery = const sortWithoutQuery =
params.sort === ProductListUrlSortField.rank params.sort === ProductListUrlSortField.rank
? ProductListUrlSortField.name ? DEFAULT_SORT_KEY
: params.sort; : params.sort;
navigate( navigate(
productListUrl({ productListUrl({
@ -249,6 +251,17 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
); );
}, [params.query]); }, [params.query]);
useEffect(() => {
if (!canBeSorted(params.sort, !!selectedChannel)) {
navigate(
productListUrl({
...params,
sort: DEFAULT_SORT_KEY
})
);
}
}, [params]);
const handleTabChange = (tab: number) => { const handleTabChange = (tab: number) => {
reset(); reset();
navigate( navigate(
@ -281,17 +294,21 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
); );
const paginationState = createPaginationState(settings.rowNumber, params); const paginationState = createPaginationState(settings.rowNumber, params);
const channelSlug = noChannel ? null : channel.slug; const channelOpts = availableChannels
const filter = getFilterVariables(params, channelSlug); ? mapNodeToChoice(availableChannels, channel => channel.slug)
const sort = getSortQueryVariables(params, channelSlug); : null;
const filter = getFilterVariables(params, !!selectedChannel);
const sort = getSortQueryVariables(params, !!selectedChannel);
const queryVariables = React.useMemo<ProductListVariables>( const queryVariables = React.useMemo<ProductListVariables>(
() => ({ () => ({
...paginationState, ...paginationState,
filter, filter,
sort sort,
channel: selectedChannel?.slug
}), }),
[params, settings.rowNumber] [params, settings.rowNumber]
); );
// TODO: When channel is undefined we should skip detailed pricing listings
const { data, loading, refetch } = useProductListQuery({ const { data, loading, refetch } = useProductListQuery({
displayLoader: true, displayLoader: true,
variables: queryVariables variables: queryVariables
@ -339,7 +356,8 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
{ {
initial: mapEdgesToItems(initialFilterProductTypes?.productTypes), initial: mapEdgesToItems(initialFilterProductTypes?.productTypes),
search: searchProductTypes search: searchProductTypes
} },
channelOpts
); );
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
@ -360,7 +378,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
availableInGridAttributes={mapEdgesToItems( availableInGridAttributes={mapEdgesToItems(
attributes?.data?.availableInGrid attributes?.data?.availableInGrid
)} )}
currencySymbol={channel?.currencyCode || ""} currencySymbol={selectedChannel?.currencyCode || ""}
currentTab={currentTab} currentTab={currentTab}
defaultSettings={defaultListSettings[ListViews.PRODUCT_LIST]} defaultSettings={defaultListSettings[ListViews.PRODUCT_LIST]}
filterOpts={filterOpts} filterOpts={filterOpts}
@ -437,7 +455,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
tabs={getFilterTabs().map(tab => tab.name)} tabs={getFilterTabs().map(tab => tab.name)}
onExport={() => openModal("export")} onExport={() => openModal("export")}
channelsCount={availableChannels?.length} channelsCount={availableChannels?.length}
selectedChannelId={channel?.id} selectedChannelId={selectedChannel?.id}
/> />
<ActionDialog <ActionDialog
open={params.action === "delete"} open={params.action === "delete"}

View file

@ -55,6 +55,7 @@ Object {
"categories": Array [ "categories": Array [
"878752", "878752",
], ],
"channel": "default-channel",
"collections": Array [ "collections": Array [
"Q29sbGVjdGlvbjoc", "Q29sbGVjdGlvbjoc",
], ],
@ -67,4 +68,4 @@ Object {
} }
`; `;
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"stockStatus=IN_STOCK&priceFrom=10&priceTo=20&categories%5B0%5D=878752&collections%5B0%5D=Q29sbGVjdGlvbjoc&productTypes%5B0%5D=UHJvZHVjdFR5cGU6MQ%3D%3D&attributes%5Bauthor%5D%5B0%5D=john-doe&attributes%5Bauthor%5D%5B1%5D=false&attributes%5Bbox-size%5D%5B0%5D=100g&attributes%5Bbox-size%5D%5B1%5D=500g&attributes%5Bbrand%5D%5B0%5D=saleor&attributes%5Bbrand%5D%5B1%5D=false&attributes%5Bcandy-box-size%5D%5B0%5D=100g&attributes%5Bcandy-box-size%5D%5B1%5D=500g&attributes%5Bcoffee-genre%5D%5B0%5D=arabica&attributes%5Bcoffee-genre%5D%5B1%5D=false&attributes%5Bcollar%5D%5B0%5D=round&attributes%5Bcollar%5D%5B1%5D=polo&attributes%5Bcolor%5D%5B0%5D=blue&attributes%5Bcolor%5D%5B1%5D=false&attributes%5Bcover%5D%5B0%5D=soft&attributes%5Bcover%5D%5B1%5D=middle-soft&attributes%5Bflavor%5D%5B0%5D=sour&attributes%5Bflavor%5D%5B1%5D=false&attributes%5Blanguage%5D%5B0%5D=english&attributes%5Blanguage%5D%5B1%5D=false&attributes%5Bpublisher%5D%5B0%5D=mirumee-press&attributes%5Bpublisher%5D%5B1%5D=false&attributes%5Bsize%5D%5B0%5D=xs&attributes%5Bsize%5D%5B1%5D=m"`; exports[`Filtering URL params should not be empty if active filters are present 2`] = `"channel=default-channel&stockStatus=IN_STOCK&priceFrom=10&priceTo=20&categories%5B0%5D=878752&collections%5B0%5D=Q29sbGVjdGlvbjoc&productTypes%5B0%5D=UHJvZHVjdFR5cGU6MQ%3D%3D&attributes%5Bauthor%5D%5B0%5D=john-doe&attributes%5Bauthor%5D%5B1%5D=false&attributes%5Bbox-size%5D%5B0%5D=100g&attributes%5Bbox-size%5D%5B1%5D=500g&attributes%5Bbrand%5D%5B0%5D=saleor&attributes%5Bbrand%5D%5B1%5D=false&attributes%5Bcandy-box-size%5D%5B0%5D=100g&attributes%5Bcandy-box-size%5D%5B1%5D=500g&attributes%5Bcoffee-genre%5D%5B0%5D=arabica&attributes%5Bcoffee-genre%5D%5B1%5D=false&attributes%5Bcollar%5D%5B0%5D=round&attributes%5Bcollar%5D%5B1%5D=polo&attributes%5Bcolor%5D%5B0%5D=blue&attributes%5Bcolor%5D%5B1%5D=false&attributes%5Bcover%5D%5B0%5D=soft&attributes%5Bcover%5D%5B1%5D=middle-soft&attributes%5Bflavor%5D%5B0%5D=sour&attributes%5Bflavor%5D%5B1%5D=false&attributes%5Blanguage%5D%5B0%5D=english&attributes%5Blanguage%5D%5B1%5D=false&attributes%5Bpublisher%5D%5B0%5D=mirumee-press&attributes%5Bpublisher%5D%5B1%5D=false&attributes%5Bsize%5D%5B0%5D=xs&attributes%5Bsize%5D%5B1%5D=m"`;

View file

@ -25,9 +25,9 @@ describe("Filtering query params", () => {
status: true.toString(), status: true.toString(),
stockStatus: StockAvailability.IN_STOCK stockStatus: StockAvailability.IN_STOCK
}; };
const filterVariables = getFilterVariables(params, "default-channel"); const filterVariables = getFilterVariables(params, true);
expect(getExistingKeys(filterVariables)).toHaveLength(3); expect(getExistingKeys(filterVariables)).toHaveLength(2);
}); });
}); });

View file

@ -1,3 +1,4 @@
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { UseSearchResult } from "@saleor/hooks/makeSearch"; import { UseSearchResult } from "@saleor/hooks/makeSearch";
import { findValueInEnum, maybe } from "@saleor/misc"; import { findValueInEnum, maybe } from "@saleor/misc";
import { import {
@ -43,7 +44,8 @@ import {
getGteLteVariables, getGteLteVariables,
getMinMaxQueryParam, getMinMaxQueryParam,
getMultipleValueQueryParam, getMultipleValueQueryParam,
getSingleEnumValueQueryParam getSingleEnumValueQueryParam,
getSingleValueQueryParam
} from "../../../utils/filters"; } from "../../../utils/filters";
import { import {
ProductListUrlFilters, ProductListUrlFilters,
@ -73,7 +75,8 @@ export function getFilterOpts(
productTypes: { productTypes: {
initial: InitialProductFilterProductTypes_productTypes_edges_node[]; initial: InitialProductFilterProductTypes_productTypes_edges_node[];
search: UseSearchResult<SearchProductTypes, SearchProductTypesVariables>; search: UseSearchResult<SearchProductTypes, SearchProductTypesVariables>;
} },
channels: SingleAutocompleteChoiceType[]
): ProductListFilterOpts { ): ProductListFilterOpts {
return { return {
attributes: attributes attributes: attributes
@ -131,6 +134,11 @@ export function getFilterOpts(
onSearchChange: categories.search.search, onSearchChange: categories.search.search,
value: maybe(() => dedupeFilter(params.categories), []) value: maybe(() => dedupeFilter(params.categories), [])
}, },
channel: {
active: params?.channel !== undefined,
choices: channels,
value: params?.channel
},
collections: { collections: {
active: !!params.collections, active: !!params.collections,
choices: mapNodeToChoice( choices: mapNodeToChoice(
@ -224,14 +232,13 @@ function getFilteredAttributeValue(
export function getFilterVariables( export function getFilterVariables(
params: ProductListUrlFilters, params: ProductListUrlFilters,
channel: string | undefined isChannelSelected: boolean
): ProductFilterInput { ): ProductFilterInput {
return { return {
attributes: getFilteredAttributeValue(params), attributes: getFilteredAttributeValue(params),
categories: params.categories !== undefined ? params.categories : null, categories: params.categories !== undefined ? params.categories : null,
channel: channel || null,
collections: params.collections !== undefined ? params.collections : null, collections: params.collections !== undefined ? params.collections : null,
price: channel price: isChannelSelected
? getGteLteVariables({ ? getGteLteVariables({
gte: parseFloat(params.priceFrom), gte: parseFloat(params.priceFrom),
lte: parseFloat(params.priceTo) lte: parseFloat(params.priceTo)
@ -298,6 +305,12 @@ export function getFilterQueryParam(
ProductListUrlFiltersEnum.stockStatus, ProductListUrlFiltersEnum.stockStatus,
StockAvailability StockAvailability
); );
case ProductFilterKeys.channel:
return getSingleValueQueryParam(
filter,
ProductListUrlFiltersEnum.channel
);
} }
} }

View file

@ -43,6 +43,16 @@ export const productListFilterOpts: ProductListFilterOpts = {
], ],
value: [categories[5].id] value: [categories[5].id]
}, },
channel: {
active: false,
value: "default-channel",
choices: [
{
value: "default-channel",
label: "Default channel"
}
]
},
collections: { collections: {
...fetchMoreProps, ...fetchMoreProps,
...searchPageProps, ...searchPageProps,

View file

@ -5,6 +5,26 @@ import {
import { ProductOrder, ProductOrderField } from "@saleor/types/globalTypes"; import { ProductOrder, ProductOrderField } from "@saleor/types/globalTypes";
import { getOrderDirection } from "@saleor/utils/sort"; import { getOrderDirection } from "@saleor/utils/sort";
export const DEFAULT_SORT_KEY = ProductListUrlSortField.name;
export function canBeSorted(
sort: ProductListUrlSortField,
isChannelSelected: boolean
) {
switch (sort) {
case ProductListUrlSortField.name:
case ProductListUrlSortField.productType:
case ProductListUrlSortField.attribute:
case ProductListUrlSortField.rank:
return true;
case ProductListUrlSortField.price:
case ProductListUrlSortField.status:
return isChannelSelected;
default:
return false;
}
}
export function getSortQueryField( export function getSortQueryField(
sort: ProductListUrlSortField sort: ProductListUrlSortField
): ProductOrderField { ): ProductOrderField {
@ -26,8 +46,12 @@ export function getSortQueryField(
export function getSortQueryVariables( export function getSortQueryVariables(
params: ProductListUrlQueryParams, params: ProductListUrlQueryParams,
channel: string isChannelSelected: boolean
): ProductOrder { ): ProductOrder {
if (!canBeSorted(params.sort, isChannelSelected)) {
return;
}
const direction = getOrderDirection(params.asc); const direction = getOrderDirection(params.asc);
if (params.sort === ProductListUrlSortField.attribute) { if (params.sort === ProductListUrlSortField.attribute) {
return { return {
@ -35,9 +59,9 @@ export function getSortQueryVariables(
direction direction
}; };
} }
const field = getSortQueryField(params.sort); const field = getSortQueryField(params.sort);
return { return {
channel,
direction, direction,
field field
}; };

File diff suppressed because it is too large Load diff

View file

@ -26,6 +26,16 @@ const props: SaleListPageProps = {
...sortPageProps, ...sortPageProps,
...tabPageProps, ...tabPageProps,
filterOpts: { filterOpts: {
channel: {
active: false,
value: "default-channel",
choices: [
{
value: "default-channel",
label: "Default channel"
}
]
},
saleType: { saleType: {
active: false, active: false,
value: DiscountValueTypeEnum.FIXED value: DiscountValueTypeEnum.FIXED

View file

@ -28,6 +28,16 @@ const props: VoucherListPageProps = {
...tabPageProps, ...tabPageProps,
...filterPageProps, ...filterPageProps,
filterOpts: { filterOpts: {
channel: {
active: false,
value: "default-channel",
choices: [
{
value: "default-channel",
label: "Default channel"
}
]
},
saleType: { saleType: {
active: false, active: false,
value: [VoucherDiscountType.FIXED, VoucherDiscountType.PERCENTAGE] value: [VoucherDiscountType.FIXED, VoucherDiscountType.PERCENTAGE]

View file

@ -1118,13 +1118,12 @@ export interface AttributeFilterInput {
filterableInStorefront?: boolean | null; filterableInStorefront?: boolean | null;
filterableInDashboard?: boolean | null; filterableInDashboard?: boolean | null;
availableInGrid?: boolean | null; availableInGrid?: boolean | null;
metadata?: (MetadataInput | null)[] | null; metadata?: (MetadataFilter | null)[] | null;
search?: string | null; search?: string | null;
ids?: (string | null)[] | null; ids?: (string | null)[] | null;
type?: AttributeTypeEnum | null; type?: AttributeTypeEnum | null;
inCollection?: string | null; inCollection?: string | null;
inCategory?: string | null; inCategory?: string | null;
channel?: string | null;
} }
export interface AttributeInput { export interface AttributeInput {
@ -1188,7 +1187,7 @@ export interface CatalogueInput {
export interface CategoryFilterInput { export interface CategoryFilterInput {
search?: string | null; search?: string | null;
metadata?: (MetadataInput | null)[] | null; metadata?: (MetadataFilter | null)[] | null;
ids?: (string | null)[] | null; ids?: (string | null)[] | null;
} }
@ -1203,7 +1202,6 @@ export interface CategoryInput {
export interface CategorySortingInput { export interface CategorySortingInput {
direction: OrderDirection; direction: OrderDirection;
channel?: string | null;
field: CategorySortField; field: CategorySortField;
} }
@ -1247,9 +1245,8 @@ export interface CollectionCreateInput {
export interface CollectionFilterInput { export interface CollectionFilterInput {
published?: CollectionPublished | null; published?: CollectionPublished | null;
search?: string | null; search?: string | null;
metadata?: (MetadataInput | null)[] | null; metadata?: (MetadataFilter | null)[] | null;
ids?: (string | null)[] | null; ids?: (string | null)[] | null;
channel?: string | null;
} }
export interface CollectionInput { export interface CollectionInput {
@ -1265,7 +1262,6 @@ export interface CollectionInput {
export interface CollectionSortingInput { export interface CollectionSortingInput {
direction: OrderDirection; direction: OrderDirection;
channel?: string | null;
field: CollectionSortField; field: CollectionSortField;
} }
@ -1279,7 +1275,7 @@ export interface CustomerFilterInput {
numberOfOrders?: IntRangeInput | null; numberOfOrders?: IntRangeInput | null;
placedOrders?: DateRangeInput | null; placedOrders?: DateRangeInput | null;
search?: string | null; search?: string | null;
metadata?: (MetadataInput | null)[] | null; metadata?: (MetadataFilter | null)[] | null;
} }
export interface CustomerInput { export interface CustomerInput {
@ -1394,6 +1390,11 @@ export interface MenuSortingInput {
field: MenuSortField; field: MenuSortField;
} }
export interface MetadataFilter {
key: string;
value?: string | null;
}
export interface MetadataInput { export interface MetadataInput {
key: string; key: string;
value: string; value: string;
@ -1417,7 +1418,7 @@ export interface OrderDraftFilterInput {
customer?: string | null; customer?: string | null;
created?: DateRangeInput | null; created?: DateRangeInput | null;
search?: string | null; search?: string | null;
metadata?: (MetadataInput | null)[] | null; metadata?: (MetadataFilter | null)[] | null;
channels?: (string | null)[] | null; channels?: (string | null)[] | null;
} }
@ -1427,7 +1428,7 @@ export interface OrderFilterInput {
customer?: string | null; customer?: string | null;
created?: DateRangeInput | null; created?: DateRangeInput | null;
search?: string | null; search?: string | null;
metadata?: (MetadataInput | null)[] | null; metadata?: (MetadataFilter | null)[] | null;
channels?: (string | null)[] | null; channels?: (string | null)[] | null;
} }
@ -1524,7 +1525,7 @@ export interface PageCreateInput {
export interface PageFilterInput { export interface PageFilterInput {
search?: string | null; search?: string | null;
metadata?: (MetadataInput | null)[] | null; metadata?: (MetadataFilter | null)[] | null;
pageTypes?: (string | null)[] | null; pageTypes?: (string | null)[] | null;
ids?: (string | null)[] | null; ids?: (string | null)[] | null;
} }
@ -1667,12 +1668,11 @@ export interface ProductFilterInput {
stockAvailability?: StockAvailability | null; stockAvailability?: StockAvailability | null;
stocks?: ProductStockFilterInput | null; stocks?: ProductStockFilterInput | null;
search?: string | null; search?: string | null;
metadata?: (MetadataInput | null)[] | null; metadata?: (MetadataFilter | null)[] | null;
price?: PriceRangeInput | null; price?: PriceRangeInput | null;
minimalPrice?: PriceRangeInput | null; minimalPrice?: PriceRangeInput | null;
productTypes?: (string | null)[] | null; productTypes?: (string | null)[] | null;
ids?: (string | null)[] | null; ids?: (string | null)[] | null;
channel?: string | null;
} }
export interface ProductInput { export interface ProductInput {
@ -1691,7 +1691,6 @@ export interface ProductInput {
export interface ProductOrder { export interface ProductOrder {
direction: OrderDirection; direction: OrderDirection;
channel?: string | null;
attributeId?: string | null; attributeId?: string | null;
field?: ProductOrderField | null; field?: ProductOrderField | null;
} }
@ -1705,7 +1704,7 @@ export interface ProductTypeFilterInput {
search?: string | null; search?: string | null;
configurable?: ProductTypeConfigurable | null; configurable?: ProductTypeConfigurable | null;
productType?: ProductTypeEnum | null; productType?: ProductTypeEnum | null;
metadata?: (MetadataInput | null)[] | null; metadata?: (MetadataFilter | null)[] | null;
ids?: (string | null)[] | null; ids?: (string | null)[] | null;
} }
@ -1798,7 +1797,6 @@ export interface SaleInput {
export interface SaleSortingInput { export interface SaleSortingInput {
direction: OrderDirection; direction: OrderDirection;
channel?: string | null;
field: SaleSortField; field: SaleSortField;
} }
@ -1987,7 +1985,6 @@ export interface VoucherInput {
export interface VoucherSortingInput { export interface VoucherSortingInput {
direction: OrderDirection; direction: OrderDirection;
channel?: string | null;
field: VoucherSortField; field: VoucherSortField;
} }

View file

@ -1,6 +1,8 @@
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo"; import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import {
ChoiceValue,
SingleAutocompleteChoiceType
} from "@saleor/components/SingleAutocompleteSelectField";
import { MetadataItem } from "@saleor/fragments/types/MetadataItem"; import { MetadataItem } from "@saleor/fragments/types/MetadataItem";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages"; import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { Node, SlugNode } from "@saleor/types"; import { Node, SlugNode } from "@saleor/types";
@ -18,48 +20,46 @@ export function mapEdgesToItems<T>(data?: EdgesType<T>): T[] {
return data.edges.map(({ node }) => node); return data.edges.map(({ node }) => node);
} }
export function mapCountriesToChoices( export function mapCountriesToChoices(countries: ShopInfo_shop_countries[]) {
countries: ShopInfo_shop_countries[]
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
return countries.map(country => ({ return countries.map(country => ({
label: country.country, label: country.country,
value: country.code value: country.code
})); }));
} }
export function mapPagesToChoices( export function mapPagesToChoices(pages: SearchPages_search_edges_node[]) {
pages: SearchPages_search_edges_node[]
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
return pages.map(page => ({ return pages.map(page => ({
label: page.title, label: page.title,
value: page.id value: page.id
})); }));
} }
export function mapNodeToChoice( type ExtendedNode = Node & Record<"name", string>;
nodes: Array<Node & Record<"name", string>> export function mapNodeToChoice<T extends ExtendedNode>(
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> { nodes: T[]
): Array<SingleAutocompleteChoiceType<string>>;
export function mapNodeToChoice<T extends ExtendedNode, K extends ChoiceValue>(
nodes: T[],
getterFn: (node: T) => K
): Array<SingleAutocompleteChoiceType<K>>;
export function mapNodeToChoice<T extends ExtendedNode>(
nodes: T[],
getterFn?: (node: T) => any
) {
if (!nodes) { if (!nodes) {
return []; return [];
} }
return nodes.map(node => ({ return nodes.map(node => ({
label: node.name, label: node.name,
value: node.id value: getterFn ? getterFn(node) : node.id
})); }));
} }
export function mapSlugNodeToChoice( export function mapSlugNodeToChoice(
nodes: Array<SlugNode & Record<"name", string>> nodes: Array<ExtendedNode & SlugNode>
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> { ): SingleAutocompleteChoiceType[] {
if (!nodes) { return mapNodeToChoice(nodes, nodes => nodes.slug);
return [];
}
return nodes.map(node => ({
label: node.name,
value: node.slug
}));
} }
export function mapMetadataItemToInput(item: MetadataItem): MetadataInput { export function mapMetadataItemToInput(item: MetadataItem): MetadataInput {

View file

@ -72,7 +72,7 @@ type GetSortQueryField<TUrlField extends string, TSortField extends string> = (
type GetSortQueryVariables< type GetSortQueryVariables<
TSortField extends string, TSortField extends string,
TParams extends Record<any, any> TParams extends Record<any, any>
> = (params: TParams, channelSlug?: string) => SortingInput<TSortField>; > = (params: TParams) => SortingInput<TSortField>;
export function createGetSortQueryVariables< export function createGetSortQueryVariables<
TUrlField extends string, TUrlField extends string,
TSortField extends string, TSortField extends string,
@ -80,14 +80,13 @@ export function createGetSortQueryVariables<
>( >(
getSortQueryField: GetSortQueryField<TUrlField, TSortField> getSortQueryField: GetSortQueryField<TUrlField, TSortField>
): GetSortQueryVariables<TSortField, TParams> { ): GetSortQueryVariables<TSortField, TParams> {
return (params: TParams, channelSlug?: string) => { return (params: TParams) => {
const field = getSortQueryField(params.sort); const field = getSortQueryField(params.sort);
if (!!field) { if (!!field) {
return { return {
direction: getOrderDirection(params.asc), direction: getOrderDirection(params.asc),
field, field
channel: channelSlug
}; };
} }