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:
parent
1b7ed5babf
commit
29f6c5496b
64 changed files with 1529 additions and 1698 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,7 +204,10 @@ 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 => {
|
||||||
|
const currentFilter = getFilterFromCurrentData(filter);
|
||||||
|
|
||||||
|
return (
|
||||||
<ExpansionPanel
|
<ExpansionPanel
|
||||||
key={filter.name}
|
key={filter.name}
|
||||||
classes={expanderClasses}
|
classes={expanderClasses}
|
||||||
|
@ -217,27 +220,31 @@ const FilterContent: React.FC<FilterContentProps> = ({
|
||||||
onClick={() => handleFilterOpen(filter)}
|
onClick={() => handleFilterOpen(filter)}
|
||||||
>
|
>
|
||||||
<FilterContentBodyNameField
|
<FilterContentBodyNameField
|
||||||
filter={getFilterFromCurrentData(filter)}
|
filter={currentFilter}
|
||||||
onFilterPropertyChange={action =>
|
onFilterPropertyChange={action =>
|
||||||
handleFilterPropertyGroupChange(action, filter)
|
handleFilterPropertyGroupChange(action, filter)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ExpansionPanelSummary>
|
</ExpansionPanelSummary>
|
||||||
|
{currentFilter.active && (
|
||||||
<FilterErrorsList
|
<FilterErrorsList
|
||||||
errors={errors}
|
errors={errors?.[filter.name]}
|
||||||
errorMessages={errorMessages}
|
errorMessages={errorMessages}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{filter.multipleFields ? (
|
{filter.multipleFields ? (
|
||||||
<CollectionWithDividers
|
<CollectionWithDividers
|
||||||
collection={filter.multipleFields}
|
collection={filter.multipleFields}
|
||||||
renderItem={filterField => (
|
renderItem={filterField => (
|
||||||
<FilterContentBody
|
<FilterContentBody
|
||||||
{...commonFilterBodyProps}
|
{...commonFilterBodyProps}
|
||||||
onFilterPropertyChange={handleMultipleFieldPropertyChange}
|
onFilterPropertyChange={
|
||||||
|
handleMultipleFieldPropertyChange
|
||||||
|
}
|
||||||
filter={{
|
filter={{
|
||||||
...getFilterFromCurrentData(filterField),
|
...getFilterFromCurrentData(filterField),
|
||||||
active: getFilterFromCurrentData(filter).active
|
active: currentFilter.active
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography>{filterField.label}</Typography>
|
<Typography>{filterField.label}</Typography>
|
||||||
|
@ -248,11 +255,12 @@ const FilterContent: React.FC<FilterContentProps> = ({
|
||||||
<FilterContentBody
|
<FilterContentBody
|
||||||
{...commonFilterBodyProps}
|
{...commonFilterBodyProps}
|
||||||
onFilterPropertyChange={onFilterPropertyChange}
|
onFilterPropertyChange={onFilterPropertyChange}
|
||||||
filter={getFilterFromCurrentData(filter)}
|
filter={currentFilter}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
18
src/components/Filter/messages.ts
Normal file
18
src/components/Filter/messages.ts
Normal 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"
|
||||||
|
}
|
||||||
|
});
|
|
@ -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[]>;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"`;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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×UsedFrom=1×UsedTo=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×UsedFrom=1×UsedTo=6&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type%5B0%5D=FIXED&type%5B1%5D=SHIPPING"`;
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default } from "./ProductAvailabilityStatusLabel";
|
||||||
|
export * from "./ProductAvailabilityStatusLabel";
|
|
@ -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"
|
||||||
|
}
|
||||||
|
});
|
|
@ -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 ? (
|
||||||
|
<ProductAvailabilityStatusLabel channel={channel} />
|
||||||
|
) : (
|
||||||
<ChannelsAvailabilityDropdown
|
<ChannelsAvailabilityDropdown
|
||||||
allChannelsCount={channelsCount}
|
allChannelsCount={channelsCount}
|
||||||
currentChannel={channel}
|
|
||||||
channels={product?.channelListings}
|
channels={product?.channelListings}
|
||||||
|
showStatus
|
||||||
/>
|
/>
|
||||||
) : (
|
))}
|
||||||
<Skeleton />
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</DisplayColumn>
|
</DisplayColumn>
|
||||||
{gridAttributesFromSettings.map(gridAttribute => (
|
{gridAttributesFromSettings.map(gridAttribute => (
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,4 +20,5 @@ export interface ProductCount {
|
||||||
|
|
||||||
export interface ProductCountVariables {
|
export interface ProductCountVariables {
|
||||||
filter?: ProductFilterInput | null;
|
filter?: ProductFilterInput | null;
|
||||||
|
channel?: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
|
@ -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"`;
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue