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 failing product update with file attribute - #1190 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

View file

@ -1986,14 +1986,14 @@
"context": "product channel publication status",
"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": {
"context": "product channel publication 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": {
"context": "product channel publication date",
"string": "Will become available on {date}"
@ -2103,6 +2103,10 @@
"context": "button",
"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": {
"context": "filter range separator",
"string": "and"
@ -2110,6 +2114,14 @@
"src_dot_components_dot_Filter_dot_FilterContent_dot_996289613": {
"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": {
"context": "image upload",
"string": "Drop here to upload"
@ -2893,6 +2905,10 @@
"context": "sale status",
"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": {
"context": "sale status",
"string": "Expired"
@ -3054,6 +3070,10 @@
"context": "voucher status",
"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": {
"context": "voucher status",
"string": "Expired"
@ -5007,8 +5027,8 @@
"string": "All Plugins"
},
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_active": {
"context": "plugin filters error messages active",
"string": "Active is not selected"
"context": "plugin filters error messages status",
"string": "Status is not selected"
},
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_channelStatusSectionSubtitle": {
"context": "status section subtitle",
@ -5199,6 +5219,18 @@
"context": "products section name",
"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": {
"string": "Category"
},
@ -5364,6 +5396,10 @@
"context": "product status",
"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": {
"context": "product is hidden",
"string": "Hidden"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,10 +2,12 @@ import { Typography } from "@material-ui/core";
import { fade, makeStyles } from "@material-ui/core/styles";
import InlineAlert from "@saleor/components/Alert/InlineAlert";
import { useStyles as useDotStyles } from "@saleor/components/StatusLabel";
import errorTracker from "@saleor/services/errorTracking";
import classNames from "classnames";
import React from "react";
import { useIntl } from "react-intl";
import { validationMessages } from "../messages";
import { FilterErrorMessages, FilterErrors, IFilterElement } from "../types";
const useStyles = makeStyles(
@ -36,7 +38,7 @@ interface FilterErrorsListProps<T extends string = string> {
}
const FilterErrorsList: React.FC<FilterErrorsListProps> = ({
filter: { name, multipleFields },
filter: { dependencies },
errors = [],
errorMessages
}) => {
@ -44,18 +46,20 @@ const FilterErrorsList: React.FC<FilterErrorsListProps> = ({
const dotClasses = useDotStyles({});
const intl = useIntl();
const hasError = (fieldName: string) =>
!!errors.find(errorName => errorName === fieldName);
const hasErrorsToShow = () => {
if (!!multipleFields?.length) {
return multipleFields.some(multipleField => hasError(multipleField.name));
const getErrorMessage = (code: string) => {
try {
return intl.formatMessage(
errorMessages?.[code] || validationMessages[code],
{ dependencies: dependencies?.join() }
);
} 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;
}
@ -63,11 +67,11 @@ const FilterErrorsList: React.FC<FilterErrorsListProps> = ({
<div className={classes.container}>
{!!errors.length && (
<InlineAlert>
{errors.map(fieldName => (
<div className={classes.itemContainer}>
{errors.map(code => (
<div className={classes.itemContainer} key={code}>
<div className={classNames(classes.dot, dotClasses.dot)} />
<Typography className={classes.listItemTitle}>
{intl.formatMessage(errorMessages?.[fieldName])}
{getErrorMessage(code)}
</Typography>
</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;
multipleFields?: IFilterElement[];
id?: string;
dependencies?: string[];
}
export interface FilterBaseFieldProps<T extends string = string> {
@ -53,3 +54,11 @@ export enum FilterType {
MULTIPLE = "MULTIPLE",
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 { FieldType, IFilterElement } from "./types";
import {
FieldType,
IFilterElement,
InvalidFilters,
ValidationErrorCode
} from "./types";
export const getByName = (nameToCompare: string) => (obj: { name: string }) =>
obj.name === nameToCompare;
@ -20,6 +25,8 @@ export const isFilterFieldValid = function<T extends string>(
case FieldType.boolean:
case FieldType.autocomplete:
return isAutocompleteFilterFieldValid(filter);
case FieldType.options:
return !!filter.value[0];
default:
return true;
@ -27,42 +34,73 @@ export const isFilterFieldValid = function<T extends string>(
};
export const isFilterValid = function<T extends string>(
resultFilters: Array<IFilterElement<T>>,
filter: IFilterElement<T>
) {
const { required, active } = filter;
if (!required || !active) {
return resultFilters;
if (!required && !active) {
return true;
}
return isFilterFieldValid(filter)
? resultFilters
: [...resultFilters, filter];
return isFilterFieldValid(filter);
};
export const extractInvalidFilters = function<T extends string>(
filtersData: Array<IFilterElement<T>>,
filtersDataStructure: Array<IFilterElement<T>>
) {
): InvalidFilters<T> {
return filtersDataStructure.reduce(
(resultFilters, { name, multipleFields }) => {
(invalidFilters, { name, multipleFields, dependencies }) => {
const filter = filtersData.find(getByName(name));
let errors: string[] = [];
const shouldExtractChildrenFields =
filter.active && !!multipleFields?.length;
if (shouldExtractChildrenFields) {
return multipleFields
.map(field => {
const dataField = filtersData.find(getByName(field.name));
return { ...dataField, active: true };
})
.reduce(isFilterValid, resultFilters);
// if filter is inactive we skip entire validation
if (!filter.active) {
return invalidFilters;
}
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 offset = 24;
export interface SingleAutocompleteChoiceType {
export type ChoiceValue = string;
export interface SingleAutocompleteChoiceType<
T extends ChoiceValue = ChoiceValue
> {
label: string;
value: any;
value: T;
}
export interface SingleAutocompleteActionType {
label: string;
@ -303,7 +306,7 @@ const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFie
<div className={classes.arrowContainer}>
<div
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
[classes.hide]: scrolledToBottom !== false
})}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
Object {
"channel": "default-channel",
"startedFrom": "2019-12-09",
"startedTo": "2019-12-38",
"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 filters = createFilterStructure(intl, {
channel: {
active: false,
choices: [
{
value: "default-channel",
label: "Default channel"
}
],
value: "default-channel"
},
saleType: {
active: false,
value: DiscountValueTypeEnum.FIXED

View file

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

View file

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

View file

@ -2,6 +2,7 @@
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
Object {
"channel": "default-channel",
"startedFrom": "2019-12-09",
"startedTo": "2019-12-38",
"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 filters = createFilterStructure(intl, {
channel: {
active: false,
choices: [
{
value: "default-channel",
label: "Default channel"
}
],
value: "default-channel"
},
saleType: {
active: false,
value: [VoucherDiscountType.FIXED, VoucherDiscountType.SHIPPING]

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import urlJoin from "url-join";
import { ConfirmButtonTransitionState } from "./components/ConfirmButton/ConfirmButton";
import { StatusType } from "./components/StatusChip/types";
import { StatusLabelProps } from "./components/StatusLabel";
import { APP_MOUNT_URI } from "./config";
import { AddressType, AddressTypeInput } from "./customers/types";
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) {
case PaymentChargeStatusEnum.PARTIALLY_CHARGED:
return {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,26 @@ import {
import { ProductOrder, ProductOrderField } from "@saleor/types/globalTypes";
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(
sort: ProductListUrlSortField
): ProductOrderField {
@ -26,8 +46,12 @@ export function getSortQueryField(
export function getSortQueryVariables(
params: ProductListUrlQueryParams,
channel: string
isChannelSelected: boolean
): ProductOrder {
if (!canBeSorted(params.sort, isChannelSelected)) {
return;
}
const direction = getOrderDirection(params.asc);
if (params.sort === ProductListUrlSortField.attribute) {
return {
@ -35,9 +59,9 @@ export function getSortQueryVariables(
direction
};
}
const field = getSortQueryField(params.sort);
return {
channel,
direction,
field
};

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -1118,13 +1118,12 @@ export interface AttributeFilterInput {
filterableInStorefront?: boolean | null;
filterableInDashboard?: boolean | null;
availableInGrid?: boolean | null;
metadata?: (MetadataInput | null)[] | null;
metadata?: (MetadataFilter | null)[] | null;
search?: string | null;
ids?: (string | null)[] | null;
type?: AttributeTypeEnum | null;
inCollection?: string | null;
inCategory?: string | null;
channel?: string | null;
}
export interface AttributeInput {
@ -1188,7 +1187,7 @@ export interface CatalogueInput {
export interface CategoryFilterInput {
search?: string | null;
metadata?: (MetadataInput | null)[] | null;
metadata?: (MetadataFilter | null)[] | null;
ids?: (string | null)[] | null;
}
@ -1203,7 +1202,6 @@ export interface CategoryInput {
export interface CategorySortingInput {
direction: OrderDirection;
channel?: string | null;
field: CategorySortField;
}
@ -1247,9 +1245,8 @@ export interface CollectionCreateInput {
export interface CollectionFilterInput {
published?: CollectionPublished | null;
search?: string | null;
metadata?: (MetadataInput | null)[] | null;
metadata?: (MetadataFilter | null)[] | null;
ids?: (string | null)[] | null;
channel?: string | null;
}
export interface CollectionInput {
@ -1265,7 +1262,6 @@ export interface CollectionInput {
export interface CollectionSortingInput {
direction: OrderDirection;
channel?: string | null;
field: CollectionSortField;
}
@ -1279,7 +1275,7 @@ export interface CustomerFilterInput {
numberOfOrders?: IntRangeInput | null;
placedOrders?: DateRangeInput | null;
search?: string | null;
metadata?: (MetadataInput | null)[] | null;
metadata?: (MetadataFilter | null)[] | null;
}
export interface CustomerInput {
@ -1394,6 +1390,11 @@ export interface MenuSortingInput {
field: MenuSortField;
}
export interface MetadataFilter {
key: string;
value?: string | null;
}
export interface MetadataInput {
key: string;
value: string;
@ -1417,7 +1418,7 @@ export interface OrderDraftFilterInput {
customer?: string | null;
created?: DateRangeInput | null;
search?: string | null;
metadata?: (MetadataInput | null)[] | null;
metadata?: (MetadataFilter | null)[] | null;
channels?: (string | null)[] | null;
}
@ -1427,7 +1428,7 @@ export interface OrderFilterInput {
customer?: string | null;
created?: DateRangeInput | null;
search?: string | null;
metadata?: (MetadataInput | null)[] | null;
metadata?: (MetadataFilter | null)[] | null;
channels?: (string | null)[] | null;
}
@ -1524,7 +1525,7 @@ export interface PageCreateInput {
export interface PageFilterInput {
search?: string | null;
metadata?: (MetadataInput | null)[] | null;
metadata?: (MetadataFilter | null)[] | null;
pageTypes?: (string | null)[] | null;
ids?: (string | null)[] | null;
}
@ -1667,12 +1668,11 @@ export interface ProductFilterInput {
stockAvailability?: StockAvailability | null;
stocks?: ProductStockFilterInput | null;
search?: string | null;
metadata?: (MetadataInput | null)[] | null;
metadata?: (MetadataFilter | null)[] | null;
price?: PriceRangeInput | null;
minimalPrice?: PriceRangeInput | null;
productTypes?: (string | null)[] | null;
ids?: (string | null)[] | null;
channel?: string | null;
}
export interface ProductInput {
@ -1691,7 +1691,6 @@ export interface ProductInput {
export interface ProductOrder {
direction: OrderDirection;
channel?: string | null;
attributeId?: string | null;
field?: ProductOrderField | null;
}
@ -1705,7 +1704,7 @@ export interface ProductTypeFilterInput {
search?: string | null;
configurable?: ProductTypeConfigurable | null;
productType?: ProductTypeEnum | null;
metadata?: (MetadataInput | null)[] | null;
metadata?: (MetadataFilter | null)[] | null;
ids?: (string | null)[] | null;
}
@ -1798,7 +1797,6 @@ export interface SaleInput {
export interface SaleSortingInput {
direction: OrderDirection;
channel?: string | null;
field: SaleSortField;
}
@ -1987,7 +1985,6 @@ export interface VoucherInput {
export interface VoucherSortingInput {
direction: OrderDirection;
channel?: string | null;
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 { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import {
ChoiceValue,
SingleAutocompleteChoiceType
} from "@saleor/components/SingleAutocompleteSelectField";
import { MetadataItem } from "@saleor/fragments/types/MetadataItem";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { Node, SlugNode } from "@saleor/types";
@ -18,48 +20,46 @@ export function mapEdgesToItems<T>(data?: EdgesType<T>): T[] {
return data.edges.map(({ node }) => node);
}
export function mapCountriesToChoices(
countries: ShopInfo_shop_countries[]
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
export function mapCountriesToChoices(countries: ShopInfo_shop_countries[]) {
return countries.map(country => ({
label: country.country,
value: country.code
}));
}
export function mapPagesToChoices(
pages: SearchPages_search_edges_node[]
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
export function mapPagesToChoices(pages: SearchPages_search_edges_node[]) {
return pages.map(page => ({
label: page.title,
value: page.id
}));
}
export function mapNodeToChoice(
nodes: Array<Node & Record<"name", string>>
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
type ExtendedNode = Node & Record<"name", string>;
export function mapNodeToChoice<T extends ExtendedNode>(
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) {
return [];
}
return nodes.map(node => ({
label: node.name,
value: node.id
value: getterFn ? getterFn(node) : node.id
}));
}
export function mapSlugNodeToChoice(
nodes: Array<SlugNode & Record<"name", string>>
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
if (!nodes) {
return [];
}
return nodes.map(node => ({
label: node.name,
value: node.slug
}));
nodes: Array<ExtendedNode & SlugNode>
): SingleAutocompleteChoiceType[] {
return mapNodeToChoice(nodes, nodes => nodes.slug);
}
export function mapMetadataItemToInput(item: MetadataItem): MetadataInput {

View file

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