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 failing product update with file attribute - #1190 by @orzechdev
|
||||
- Fix breaking select popups in filters - #1193 by @orzechdev
|
||||
- Create channel filters in product, sales and voucher lists - #1187 by @jwm0
|
||||
- Add generic filter validation - #1187 by @jwm0
|
||||
|
||||
# 2.11.1
|
||||
|
||||
|
|
|
@ -1986,14 +1986,14 @@
|
|||
"context": "product channel publication status",
|
||||
"string": "hidden"
|
||||
},
|
||||
"src_dot_components_dot_ChannelsAvailabilityDropdown_dot_1484966255": {
|
||||
"context": "product status title",
|
||||
"string": "Available in {count}/{allCount}"
|
||||
},
|
||||
"src_dot_components_dot_ChannelsAvailabilityDropdown_dot_1702481199": {
|
||||
"context": "product channel publication date",
|
||||
"string": "published since {date}"
|
||||
},
|
||||
"src_dot_components_dot_ChannelsAvailabilityDropdown_dot_1944644572": {
|
||||
"context": "product status title",
|
||||
"string": "{count}/{allCount} channels"
|
||||
},
|
||||
"src_dot_components_dot_ChannelsAvailabilityDropdown_dot_3285520461": {
|
||||
"context": "product channel publication date",
|
||||
"string": "Will become available on {date}"
|
||||
|
@ -2103,6 +2103,10 @@
|
|||
"context": "button",
|
||||
"string": "Filters"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_DEPENDENCIES_MISSING": {
|
||||
"context": "filters error messages dependencies missing",
|
||||
"string": "Filter requires other filters: {dependencies}"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_FilterContent_dot_2779594451": {
|
||||
"context": "filter range separator",
|
||||
"string": "and"
|
||||
|
@ -2110,6 +2114,14 @@
|
|||
"src_dot_components_dot_Filter_dot_FilterContent_dot_996289613": {
|
||||
"string": "Filters"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_UNKNOWN_ERROR": {
|
||||
"context": "filters error messages unknown error",
|
||||
"string": "Unknown error occurred"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_VALUE_REQUIRED": {
|
||||
"context": "filters error messages value required",
|
||||
"string": "Choose a value"
|
||||
},
|
||||
"src_dot_components_dot_ImageUpload_dot_1731007575": {
|
||||
"context": "image upload",
|
||||
"string": "Drop here to upload"
|
||||
|
@ -2893,6 +2905,10 @@
|
|||
"context": "sale status",
|
||||
"string": "Active"
|
||||
},
|
||||
"src_dot_discounts_dot_components_dot_SaleListPage_dot_channel": {
|
||||
"context": "sale channel",
|
||||
"string": "Channel"
|
||||
},
|
||||
"src_dot_discounts_dot_components_dot_SaleListPage_dot_expired": {
|
||||
"context": "sale status",
|
||||
"string": "Expired"
|
||||
|
@ -3054,6 +3070,10 @@
|
|||
"context": "voucher status",
|
||||
"string": "Active"
|
||||
},
|
||||
"src_dot_discounts_dot_components_dot_VoucherListPage_dot_channel": {
|
||||
"context": "voucher channel",
|
||||
"string": "Channel"
|
||||
},
|
||||
"src_dot_discounts_dot_components_dot_VoucherListPage_dot_expired": {
|
||||
"context": "voucher status",
|
||||
"string": "Expired"
|
||||
|
@ -5007,8 +5027,8 @@
|
|||
"string": "All Plugins"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_active": {
|
||||
"context": "plugin filters error messages active",
|
||||
"string": "Active is not selected"
|
||||
"context": "plugin filters error messages status",
|
||||
"string": "Status is not selected"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_channelStatusSectionSubtitle": {
|
||||
"context": "status section subtitle",
|
||||
|
@ -5199,6 +5219,18 @@
|
|||
"context": "products section name",
|
||||
"string": "Products"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductAvailabilityStatusLabel_dot_published": {
|
||||
"context": "product publication date",
|
||||
"string": "Published on {date}"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductAvailabilityStatusLabel_dot_unpublished": {
|
||||
"context": "product publication date",
|
||||
"string": "Unpublished"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductAvailabilityStatusLabel_dot_willBePublished": {
|
||||
"context": "product publication date",
|
||||
"string": "Becomes published on {date}"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductCategoryAndCollectionsForm_dot_1755013298": {
|
||||
"string": "Category"
|
||||
},
|
||||
|
@ -5364,6 +5396,10 @@
|
|||
"context": "product status",
|
||||
"string": "Available"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductListPage_dot_channel": {
|
||||
"context": "sales channel",
|
||||
"string": "Channel"
|
||||
},
|
||||
"src_dot_products_dot_components_dot_ProductListPage_dot_hidden": {
|
||||
"context": "product is hidden",
|
||||
"string": "Hidden"
|
||||
|
|
|
@ -506,13 +506,12 @@ input AttributeFilterInput {
|
|||
filterableInStorefront: Boolean
|
||||
filterableInDashboard: Boolean
|
||||
availableInGrid: Boolean
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
search: String
|
||||
ids: [ID]
|
||||
type: AttributeTypeEnum
|
||||
inCollection: ID
|
||||
inCategory: ID
|
||||
channel: String
|
||||
}
|
||||
|
||||
input AttributeInput {
|
||||
|
@ -777,7 +776,7 @@ type CategoryDelete {
|
|||
|
||||
input CategoryFilterInput {
|
||||
search: String
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
ids: [ID]
|
||||
}
|
||||
|
||||
|
@ -798,7 +797,6 @@ enum CategorySortField {
|
|||
|
||||
input CategorySortingInput {
|
||||
direction: OrderDirection!
|
||||
channel: String
|
||||
field: CategorySortField!
|
||||
}
|
||||
|
||||
|
@ -1230,9 +1228,8 @@ enum CollectionErrorCode {
|
|||
input CollectionFilterInput {
|
||||
published: CollectionPublished
|
||||
search: String
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
ids: [ID]
|
||||
channel: String
|
||||
}
|
||||
|
||||
input CollectionInput {
|
||||
|
@ -1272,7 +1269,6 @@ enum CollectionSortField {
|
|||
|
||||
input CollectionSortingInput {
|
||||
direction: OrderDirection!
|
||||
channel: String
|
||||
field: CollectionSortField!
|
||||
}
|
||||
|
||||
|
@ -1670,7 +1666,7 @@ input CustomerFilterInput {
|
|||
numberOfOrders: IntRangeInput
|
||||
placedOrders: DateRangeInput
|
||||
search: String
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
}
|
||||
|
||||
input CustomerInput {
|
||||
|
@ -2523,7 +2519,7 @@ enum MenuErrorCode {
|
|||
input MenuFilterInput {
|
||||
search: String
|
||||
slug: [String]
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
}
|
||||
|
||||
input MenuInput {
|
||||
|
@ -2588,7 +2584,7 @@ type MenuItemDelete {
|
|||
|
||||
input MenuItemFilterInput {
|
||||
search: String
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
}
|
||||
|
||||
input MenuItemInput {
|
||||
|
@ -2674,6 +2670,11 @@ enum MetadataErrorCode {
|
|||
REQUIRED
|
||||
}
|
||||
|
||||
input MetadataFilter {
|
||||
key: String!
|
||||
value: String
|
||||
}
|
||||
|
||||
input MetadataInput {
|
||||
key: String!
|
||||
value: String!
|
||||
|
@ -3132,7 +3133,7 @@ input OrderDraftFilterInput {
|
|||
customer: String
|
||||
created: DateRangeInput
|
||||
search: String
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
channels: [ID]
|
||||
}
|
||||
|
||||
|
@ -3293,7 +3294,7 @@ input OrderFilterInput {
|
|||
customer: String
|
||||
created: DateRangeInput
|
||||
search: String
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
channels: [ID]
|
||||
}
|
||||
|
||||
|
@ -3624,7 +3625,7 @@ enum PageErrorCode {
|
|||
|
||||
input PageFilterInput {
|
||||
search: String
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
pageTypes: [ID]
|
||||
ids: [ID]
|
||||
}
|
||||
|
@ -4284,12 +4285,11 @@ input ProductFilterInput {
|
|||
stockAvailability: StockAvailability
|
||||
stocks: ProductStockFilterInput
|
||||
search: String
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
price: PriceRangeInput
|
||||
minimalPrice: PriceRangeInput
|
||||
productTypes: [ID]
|
||||
ids: [ID]
|
||||
channel: String
|
||||
}
|
||||
|
||||
type ProductImage {
|
||||
|
@ -4374,7 +4374,6 @@ input ProductMediaUpdateInput {
|
|||
|
||||
input ProductOrder {
|
||||
direction: OrderDirection!
|
||||
channel: String
|
||||
attributeId: ID
|
||||
field: ProductOrderField
|
||||
}
|
||||
|
@ -4499,7 +4498,7 @@ input ProductTypeFilterInput {
|
|||
search: String
|
||||
configurable: ProductTypeConfigurable
|
||||
productType: ProductTypeEnum
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
ids: [ID]
|
||||
}
|
||||
|
||||
|
@ -4644,7 +4643,7 @@ type ProductVariantDelete {
|
|||
input ProductVariantFilterInput {
|
||||
search: String
|
||||
sku: [String]
|
||||
metadata: [MetadataInput]
|
||||
metadata: [MetadataFilter]
|
||||
}
|
||||
|
||||
input ProductVariantInput {
|
||||
|
@ -4781,7 +4780,7 @@ type Query {
|
|||
checkoutLines(before: String, after: String, first: Int, last: Int): CheckoutLineCountableConnection
|
||||
channel(id: ID): Channel
|
||||
channels: [Channel!]
|
||||
attributes(filter: AttributeFilterInput, sortBy: AttributeSortingInput, before: String, after: String, first: Int, last: Int): AttributeCountableConnection
|
||||
attributes(filter: AttributeFilterInput, sortBy: AttributeSortingInput, channel: String, before: String, after: String, first: Int, last: Int): AttributeCountableConnection
|
||||
attribute(id: ID, slug: String): Attribute
|
||||
appsInstallations: [AppInstallation!]!
|
||||
apps(filter: AppFilterInput, sortBy: AppSortingInput, before: String, after: String, first: Int, last: Int): AppCountableConnection
|
||||
|
@ -4938,7 +4937,6 @@ enum SaleSortField {
|
|||
|
||||
input SaleSortingInput {
|
||||
direction: OrderDirection!
|
||||
channel: String
|
||||
field: SaleSortField!
|
||||
}
|
||||
|
||||
|
@ -5885,7 +5883,6 @@ enum VoucherSortField {
|
|||
|
||||
input VoucherSortingInput {
|
||||
direction: OrderDirection!
|
||||
channel: String
|
||||
field: VoucherSortField!
|
||||
}
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ const CategoryList: React.FC<CategoryListProps> = props => {
|
|||
}
|
||||
arrowPosition="right"
|
||||
className={classes.colName}
|
||||
disableClick={!isRoot}
|
||||
disabled={!isRoot}
|
||||
onClick={() => isRoot && onSort(CategoryListUrlSortField.name)}
|
||||
>
|
||||
<FormattedMessage defaultMessage="Category Name" />
|
||||
|
@ -106,7 +106,7 @@ const CategoryList: React.FC<CategoryListProps> = props => {
|
|||
: undefined
|
||||
}
|
||||
className={classes.colSubcategories}
|
||||
disableClick={!isRoot}
|
||||
disabled={!isRoot}
|
||||
onClick={() =>
|
||||
isRoot && onSort(CategoryListUrlSortField.subcategoryCount)
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ const CategoryList: React.FC<CategoryListProps> = props => {
|
|||
: undefined
|
||||
}
|
||||
className={classes.colProducts}
|
||||
disableClick={!isRoot}
|
||||
disabled={!isRoot}
|
||||
onClick={() =>
|
||||
isRoot && onSort(CategoryListUrlSortField.productCount)
|
||||
}
|
||||
|
|
|
@ -194,7 +194,6 @@ export const CategoryProductList: React.FC<CategoryProductListProps> = props =>
|
|||
) : product?.channelListings !== undefined ? (
|
||||
<ChannelsAvailabilityDropdown
|
||||
allChannelsCount={channelsCount}
|
||||
currentChannel={channel || product?.channelListings[0]}
|
||||
channels={product?.channelListings}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -102,7 +102,6 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
|
|||
<CollectionProducts
|
||||
disabled={disabled}
|
||||
channelsCount={channelsCount}
|
||||
selectedChannelId={selectedChannelId}
|
||||
collection={collection}
|
||||
{...collectionProductsProps}
|
||||
/>
|
||||
|
|
|
@ -184,8 +184,8 @@ const CollectionList: React.FC<CollectionListProps> = props => {
|
|||
channel ? (
|
||||
<ChannelsAvailabilityDropdown
|
||||
allChannelsCount={channelsCount}
|
||||
currentChannel={channel}
|
||||
channels={collection?.channelListings}
|
||||
showStatus
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
|
|
|
@ -23,7 +23,7 @@ import React from "react";
|
|||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { maybe, renderCollection } from "../../../misc";
|
||||
import { ChannelProps, ListActions, PageListProps } from "../../../types";
|
||||
import { ListActions, PageListProps } from "../../../types";
|
||||
import { CollectionDetails_collection } from "../../types/CollectionDetails";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
|
@ -57,10 +57,7 @@ const useStyles = makeStyles(
|
|||
{ name: "CollectionProducts" }
|
||||
);
|
||||
|
||||
export interface CollectionProductsProps
|
||||
extends PageListProps,
|
||||
ListActions,
|
||||
ChannelProps {
|
||||
export interface CollectionProductsProps extends PageListProps, ListActions {
|
||||
collection: CollectionDetails_collection;
|
||||
channelsCount: number;
|
||||
onProductUnassign: (id: string, event: React.MouseEvent<any>) => void;
|
||||
|
@ -79,7 +76,6 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
|
|||
onProductUnassign,
|
||||
onRowClick,
|
||||
pageInfo,
|
||||
selectedChannelId,
|
||||
isChecked,
|
||||
selected,
|
||||
toggle,
|
||||
|
@ -170,10 +166,6 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
|
|||
mapEdgesToItems(collection?.products),
|
||||
product => {
|
||||
const isSelected = product ? isChecked(product.id) : false;
|
||||
const channel =
|
||||
product?.channelListings.find(
|
||||
listing => listing.channel.id === selectedChannelId
|
||||
) || product?.channelListings[0];
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
|
@ -209,7 +201,6 @@ const CollectionProducts: React.FC<CollectionProductsProps> = props => {
|
|||
) : product?.channelListings !== undefined ? (
|
||||
<ChannelsAvailabilityDropdown
|
||||
allChannelsCount={channelsCount}
|
||||
currentChannel={channel}
|
||||
channels={product?.channelListings}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -10,7 +10,7 @@ import ChannelsAvailabilityDropdown, {
|
|||
const props: ChannelsAvailabilityDropdownProps = {
|
||||
allChannelsCount: 6,
|
||||
channels: productChannels,
|
||||
currentChannel: productChannels[0]
|
||||
showStatus: true
|
||||
};
|
||||
|
||||
storiesOf("Generics / ChannelsAvailabilityDropdown", module)
|
||||
|
|
|
@ -16,7 +16,7 @@ type Channels = Pick<
|
|||
export interface ChannelsAvailabilityDropdownProps {
|
||||
allChannelsCount: number;
|
||||
channels: Channels[];
|
||||
currentChannel: Channels;
|
||||
showStatus?: boolean;
|
||||
}
|
||||
|
||||
const isActive = (channelData: Channels) => channelData?.isPublished;
|
||||
|
@ -24,7 +24,7 @@ const isActive = (channelData: Channels) => channelData?.isPublished;
|
|||
export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdownProps> = ({
|
||||
allChannelsCount,
|
||||
channels,
|
||||
currentChannel
|
||||
showStatus = false
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
|
@ -32,8 +32,12 @@ export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdown
|
|||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
|
||||
const handleClick = event => setAnchorEl(event.currentTarget);
|
||||
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
const activeInAllChannels = React.useMemo(
|
||||
() => showStatus && channels.every(isActive),
|
||||
[channels, showStatus]
|
||||
);
|
||||
|
||||
return (
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<div
|
||||
|
@ -45,7 +49,7 @@ export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdown
|
|||
<StatusLabel
|
||||
label={intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Available in {count}/{allCount}",
|
||||
defaultMessage: "{count}/{allCount} channels",
|
||||
description: "product status title"
|
||||
},
|
||||
{
|
||||
|
@ -53,7 +57,9 @@ export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdown
|
|||
count: channels.length
|
||||
}
|
||||
)}
|
||||
status={isActive(currentChannel) ? "success" : "error"}
|
||||
status={
|
||||
showStatus ? (activeInAllChannels ? "success" : "error") : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Menu
|
||||
|
|
|
@ -12,7 +12,12 @@ import React, { useState } from "react";
|
|||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { FilterContent } from ".";
|
||||
import { FilterErrorMessages, IFilter, IFilterElement } from "./types";
|
||||
import {
|
||||
FilterErrorMessages,
|
||||
IFilter,
|
||||
IFilterElement,
|
||||
InvalidFilters
|
||||
} from "./types";
|
||||
import useFilter from "./useFilter";
|
||||
import { extractInvalidFilters } from "./utils";
|
||||
|
||||
|
@ -103,7 +108,7 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
|
||||
const anchor = React.useRef<HTMLDivElement>();
|
||||
const [isFilterMenuOpened, setFilterMenuOpened] = useState(false);
|
||||
const [filterErrors, setFilterErrors] = useState<string[]>([]);
|
||||
const [filterErrors, setFilterErrors] = useState<InvalidFilters<string>>({});
|
||||
const [data, dispatch, reset] = useFilter(menu);
|
||||
|
||||
const isFilterActive = menu.some(filterElement => filterElement.active);
|
||||
|
@ -111,17 +116,21 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
const handleSubmit = () => {
|
||||
const invalidFilters = extractInvalidFilters(data, menu);
|
||||
|
||||
if (!!invalidFilters.length) {
|
||||
const parsedFilterErrors = invalidFilters.map(({ name }) => name);
|
||||
setFilterErrors(parsedFilterErrors);
|
||||
if (Object.keys(invalidFilters).length > 0) {
|
||||
setFilterErrors(invalidFilters);
|
||||
return;
|
||||
}
|
||||
|
||||
setFilterErrors([]);
|
||||
setFilterErrors({});
|
||||
onFilterAdd(data);
|
||||
setFilterMenuOpened(false);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
reset();
|
||||
setFilterErrors({});
|
||||
};
|
||||
|
||||
return (
|
||||
<ClickAwayListener
|
||||
onClickAway={event => {
|
||||
|
@ -196,7 +205,7 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
dataStructure={menu}
|
||||
currencySymbol={currencySymbol}
|
||||
filters={data}
|
||||
onClear={reset}
|
||||
onClear={handleClear}
|
||||
onFilterPropertyChange={dispatch}
|
||||
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||
onSubmit={handleSubmit}
|
||||
|
|
|
@ -16,9 +16,9 @@ import { FilterReducerAction } from "../reducer";
|
|||
import {
|
||||
FieldType,
|
||||
FilterErrorMessages,
|
||||
FilterErrors,
|
||||
IFilter,
|
||||
IFilterElement
|
||||
IFilterElement,
|
||||
InvalidFilters
|
||||
} from "../types";
|
||||
import FilterContentBody, { FilterContentBodyProps } from "./FilterContentBody";
|
||||
import FilterContentBodyNameField from "./FilterContentBodyNameField";
|
||||
|
@ -80,7 +80,7 @@ export interface FilterContentProps<T extends string = string> {
|
|||
onSubmit: () => void;
|
||||
currencySymbol?: string;
|
||||
dataStructure: IFilter<T>;
|
||||
errors?: FilterErrors;
|
||||
errors?: InvalidFilters<T>;
|
||||
errorMessages?: FilterErrorMessages<T>;
|
||||
}
|
||||
|
||||
|
@ -204,55 +204,63 @@ const FilterContent: React.FC<FilterContentProps> = ({
|
|||
<Hr />
|
||||
{dataStructure
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
.map(filter => (
|
||||
<ExpansionPanel
|
||||
key={filter.name}
|
||||
classes={expanderClasses}
|
||||
data-test="channel-availability-item"
|
||||
expanded={filter.name === openedFilter?.name}
|
||||
>
|
||||
<ExpansionPanelSummary
|
||||
expandIcon={<IconChevronDown />}
|
||||
classes={summaryClasses}
|
||||
onClick={() => handleFilterOpen(filter)}
|
||||
.map(filter => {
|
||||
const currentFilter = getFilterFromCurrentData(filter);
|
||||
|
||||
return (
|
||||
<ExpansionPanel
|
||||
key={filter.name}
|
||||
classes={expanderClasses}
|
||||
data-test="channel-availability-item"
|
||||
expanded={filter.name === openedFilter?.name}
|
||||
>
|
||||
<FilterContentBodyNameField
|
||||
filter={getFilterFromCurrentData(filter)}
|
||||
onFilterPropertyChange={action =>
|
||||
handleFilterPropertyGroupChange(action, filter)
|
||||
}
|
||||
/>
|
||||
</ExpansionPanelSummary>
|
||||
<FilterErrorsList
|
||||
errors={errors}
|
||||
errorMessages={errorMessages}
|
||||
filter={filter}
|
||||
/>
|
||||
{filter.multipleFields ? (
|
||||
<CollectionWithDividers
|
||||
collection={filter.multipleFields}
|
||||
renderItem={filterField => (
|
||||
<FilterContentBody
|
||||
{...commonFilterBodyProps}
|
||||
onFilterPropertyChange={handleMultipleFieldPropertyChange}
|
||||
filter={{
|
||||
...getFilterFromCurrentData(filterField),
|
||||
active: getFilterFromCurrentData(filter).active
|
||||
}}
|
||||
>
|
||||
<Typography>{filterField.label}</Typography>
|
||||
</FilterContentBody>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<FilterContentBody
|
||||
{...commonFilterBodyProps}
|
||||
onFilterPropertyChange={onFilterPropertyChange}
|
||||
filter={getFilterFromCurrentData(filter)}
|
||||
/>
|
||||
)}
|
||||
</ExpansionPanel>
|
||||
))}
|
||||
<ExpansionPanelSummary
|
||||
expandIcon={<IconChevronDown />}
|
||||
classes={summaryClasses}
|
||||
onClick={() => handleFilterOpen(filter)}
|
||||
>
|
||||
<FilterContentBodyNameField
|
||||
filter={currentFilter}
|
||||
onFilterPropertyChange={action =>
|
||||
handleFilterPropertyGroupChange(action, filter)
|
||||
}
|
||||
/>
|
||||
</ExpansionPanelSummary>
|
||||
{currentFilter.active && (
|
||||
<FilterErrorsList
|
||||
errors={errors?.[filter.name]}
|
||||
errorMessages={errorMessages}
|
||||
filter={filter}
|
||||
/>
|
||||
)}
|
||||
{filter.multipleFields ? (
|
||||
<CollectionWithDividers
|
||||
collection={filter.multipleFields}
|
||||
renderItem={filterField => (
|
||||
<FilterContentBody
|
||||
{...commonFilterBodyProps}
|
||||
onFilterPropertyChange={
|
||||
handleMultipleFieldPropertyChange
|
||||
}
|
||||
filter={{
|
||||
...getFilterFromCurrentData(filterField),
|
||||
active: currentFilter.active
|
||||
}}
|
||||
>
|
||||
<Typography>{filterField.label}</Typography>
|
||||
</FilterContentBody>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<FilterContentBody
|
||||
{...commonFilterBodyProps}
|
||||
onFilterPropertyChange={onFilterPropertyChange}
|
||||
filter={currentFilter}
|
||||
/>
|
||||
)}
|
||||
</ExpansionPanel>
|
||||
);
|
||||
})}
|
||||
</form>
|
||||
</Paper>
|
||||
);
|
||||
|
|
|
@ -2,10 +2,12 @@ import { Typography } from "@material-ui/core";
|
|||
import { fade, makeStyles } from "@material-ui/core/styles";
|
||||
import InlineAlert from "@saleor/components/Alert/InlineAlert";
|
||||
import { useStyles as useDotStyles } from "@saleor/components/StatusLabel";
|
||||
import errorTracker from "@saleor/services/errorTracking";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { validationMessages } from "../messages";
|
||||
import { FilterErrorMessages, FilterErrors, IFilterElement } from "../types";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
|
@ -36,7 +38,7 @@ interface FilterErrorsListProps<T extends string = string> {
|
|||
}
|
||||
|
||||
const FilterErrorsList: React.FC<FilterErrorsListProps> = ({
|
||||
filter: { name, multipleFields },
|
||||
filter: { dependencies },
|
||||
errors = [],
|
||||
errorMessages
|
||||
}) => {
|
||||
|
@ -44,18 +46,20 @@ const FilterErrorsList: React.FC<FilterErrorsListProps> = ({
|
|||
const dotClasses = useDotStyles({});
|
||||
const intl = useIntl();
|
||||
|
||||
const hasError = (fieldName: string) =>
|
||||
!!errors.find(errorName => errorName === fieldName);
|
||||
|
||||
const hasErrorsToShow = () => {
|
||||
if (!!multipleFields?.length) {
|
||||
return multipleFields.some(multipleField => hasError(multipleField.name));
|
||||
const getErrorMessage = (code: string) => {
|
||||
try {
|
||||
return intl.formatMessage(
|
||||
errorMessages?.[code] || validationMessages[code],
|
||||
{ dependencies: dependencies?.join() }
|
||||
);
|
||||
} catch (e) {
|
||||
errorTracker.captureException(e);
|
||||
console.warn("Translation missing for filter error code: ", code);
|
||||
return intl.formatMessage(validationMessages.UNKNOWN_ERROR);
|
||||
}
|
||||
|
||||
return hasError(name);
|
||||
};
|
||||
|
||||
if (!errors.length || !hasErrorsToShow()) {
|
||||
if (!errors.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -63,11 +67,11 @@ const FilterErrorsList: React.FC<FilterErrorsListProps> = ({
|
|||
<div className={classes.container}>
|
||||
{!!errors.length && (
|
||||
<InlineAlert>
|
||||
{errors.map(fieldName => (
|
||||
<div className={classes.itemContainer}>
|
||||
{errors.map(code => (
|
||||
<div className={classes.itemContainer} key={code}>
|
||||
<div className={classNames(classes.dot, dotClasses.dot)} />
|
||||
<Typography className={classes.listItemTitle}>
|
||||
{intl.formatMessage(errorMessages?.[fieldName])}
|
||||
{getErrorMessage(code)}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
|
|
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;
|
||||
multipleFields?: IFilterElement[];
|
||||
id?: string;
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
export interface FilterBaseFieldProps<T extends string = string> {
|
||||
|
@ -53,3 +54,11 @@ export enum FilterType {
|
|||
MULTIPLE = "MULTIPLE",
|
||||
SINGULAR = "SINGULAR"
|
||||
}
|
||||
|
||||
export enum ValidationErrorCode {
|
||||
VALUE_REQUIRED = "VALUE_REQUIRED",
|
||||
DEPENDENCIES_MISSING = "DEPENDENCIES_MISSING",
|
||||
UNKNOWN_ERROR = "UNKNOWN_ERROR"
|
||||
}
|
||||
|
||||
export type InvalidFilters<T extends string> = Record<T, string[]>;
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import compact from "lodash/compact";
|
||||
|
||||
import { FieldType, IFilterElement } from "./types";
|
||||
import {
|
||||
FieldType,
|
||||
IFilterElement,
|
||||
InvalidFilters,
|
||||
ValidationErrorCode
|
||||
} from "./types";
|
||||
|
||||
export const getByName = (nameToCompare: string) => (obj: { name: string }) =>
|
||||
obj.name === nameToCompare;
|
||||
|
@ -20,6 +25,8 @@ export const isFilterFieldValid = function<T extends string>(
|
|||
case FieldType.boolean:
|
||||
case FieldType.autocomplete:
|
||||
return isAutocompleteFilterFieldValid(filter);
|
||||
case FieldType.options:
|
||||
return !!filter.value[0];
|
||||
|
||||
default:
|
||||
return true;
|
||||
|
@ -27,42 +34,73 @@ export const isFilterFieldValid = function<T extends string>(
|
|||
};
|
||||
|
||||
export const isFilterValid = function<T extends string>(
|
||||
resultFilters: Array<IFilterElement<T>>,
|
||||
filter: IFilterElement<T>
|
||||
) {
|
||||
const { required, active } = filter;
|
||||
|
||||
if (!required || !active) {
|
||||
return resultFilters;
|
||||
if (!required && !active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isFilterFieldValid(filter)
|
||||
? resultFilters
|
||||
: [...resultFilters, filter];
|
||||
return isFilterFieldValid(filter);
|
||||
};
|
||||
|
||||
export const extractInvalidFilters = function<T extends string>(
|
||||
filtersData: Array<IFilterElement<T>>,
|
||||
filtersDataStructure: Array<IFilterElement<T>>
|
||||
) {
|
||||
): InvalidFilters<T> {
|
||||
return filtersDataStructure.reduce(
|
||||
(resultFilters, { name, multipleFields }) => {
|
||||
(invalidFilters, { name, multipleFields, dependencies }) => {
|
||||
const filter = filtersData.find(getByName(name));
|
||||
let errors: string[] = [];
|
||||
|
||||
const shouldExtractChildrenFields =
|
||||
filter.active && !!multipleFields?.length;
|
||||
|
||||
if (shouldExtractChildrenFields) {
|
||||
return multipleFields
|
||||
.map(field => {
|
||||
const dataField = filtersData.find(getByName(field.name));
|
||||
return { ...dataField, active: true };
|
||||
})
|
||||
.reduce(isFilterValid, resultFilters);
|
||||
// if filter is inactive we skip entire validation
|
||||
if (!filter.active) {
|
||||
return invalidFilters;
|
||||
}
|
||||
|
||||
return isFilterValid(resultFilters, filter);
|
||||
if (!isFilterValid(filter)) {
|
||||
errors.push(ValidationErrorCode.VALUE_REQUIRED);
|
||||
}
|
||||
|
||||
if (shouldExtractChildrenFields) {
|
||||
const multipleFieldErrors = multipleFields
|
||||
.map(field => {
|
||||
const filter = filtersData.find(getByName(field.name));
|
||||
return { ...filter, active: true };
|
||||
})
|
||||
.filter(el => !isFilterValid(el))
|
||||
.map(({ name }) => name);
|
||||
|
||||
errors = [...errors, ...multipleFieldErrors];
|
||||
}
|
||||
|
||||
// check if filter depends on other filters
|
||||
if (dependencies?.length > 0) {
|
||||
const deps = dependencies
|
||||
.map(name => {
|
||||
const filter = filtersData.find(getByName(name));
|
||||
return { ...filter, required: true };
|
||||
})
|
||||
.filter(el => !isFilterValid(el));
|
||||
|
||||
if (deps.length > 0) {
|
||||
errors.push(ValidationErrorCode.DEPENDENCIES_MISSING);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length === 0) {
|
||||
return invalidFilters;
|
||||
}
|
||||
|
||||
return {
|
||||
...invalidFilters,
|
||||
[name]: errors
|
||||
};
|
||||
},
|
||||
[]
|
||||
{} as InvalidFilters<T>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,9 +23,12 @@ const menuItemHeight = 46;
|
|||
const maxMenuItems = 5;
|
||||
const offset = 24;
|
||||
|
||||
export interface SingleAutocompleteChoiceType {
|
||||
export type ChoiceValue = string;
|
||||
export interface SingleAutocompleteChoiceType<
|
||||
T extends ChoiceValue = ChoiceValue
|
||||
> {
|
||||
label: string;
|
||||
value: any;
|
||||
value: T;
|
||||
}
|
||||
export interface SingleAutocompleteActionType {
|
||||
label: string;
|
||||
|
@ -303,7 +306,7 @@ const SingleAutocompleteSelectFieldContent: React.FC<SingleAutocompleteSelectFie
|
|||
<div className={classes.arrowContainer}>
|
||||
<div
|
||||
className={classNames(classes.arrowInnerContainer, {
|
||||
// Needs to be explicitely compared to false because
|
||||
// Needs to be explicitly compared to false because
|
||||
// scrolledToBottom can be either true, false or undefined
|
||||
[classes.hide]: scrolledToBottom !== false
|
||||
})}
|
||||
|
|
|
@ -11,7 +11,9 @@ export const useStyles = makeStyles(
|
|||
const dot = {
|
||||
borderRadius: "100%",
|
||||
height: 8,
|
||||
width: 8
|
||||
minHeight: 8,
|
||||
width: 8,
|
||||
minWidth: 8
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -56,9 +58,9 @@ export const useStyles = makeStyles(
|
|||
{ name: "StatusLabel" }
|
||||
);
|
||||
|
||||
interface StatusLabelProps {
|
||||
export interface StatusLabelProps {
|
||||
label: string | React.ReactNode;
|
||||
status: "success" | "alert" | "neutral" | "error" | string;
|
||||
status: "success" | "alert" | "neutral" | "error" | undefined;
|
||||
subtitle?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@ const useStyles = makeStyles(
|
|||
arrowUp: {
|
||||
transform: "rotate(180deg)"
|
||||
},
|
||||
disableClick: {
|
||||
disabled: {
|
||||
opacity: 0.7,
|
||||
"&&": {
|
||||
cursor: "unset"
|
||||
}
|
||||
|
@ -54,8 +55,8 @@ export type TableCellHeaderArrowPosition = "left" | "right";
|
|||
export interface TableCellHeaderProps extends TableCellProps {
|
||||
arrowPosition?: TableCellHeaderArrowPosition;
|
||||
direction?: TableCellHeaderArrowDirection;
|
||||
disableClick?: boolean;
|
||||
textAlign?: "left" | "center" | "right";
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const TableCellHeader: React.FC<TableCellHeaderProps> = props => {
|
||||
|
@ -65,16 +66,22 @@ const TableCellHeader: React.FC<TableCellHeaderProps> = props => {
|
|||
children,
|
||||
className,
|
||||
direction,
|
||||
disableClick,
|
||||
textAlign,
|
||||
disabled = false,
|
||||
onClick,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
{...rest}
|
||||
onClick={e => {
|
||||
if (!disabled) {
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
className={classNames(classes.root, className, {
|
||||
[classes.disableClick]: disableClick
|
||||
[classes.disabled]: disabled
|
||||
})}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -23,14 +23,11 @@ import React from "react";
|
|||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { maybe, renderCollection } from "../../../misc";
|
||||
import { ChannelProps, ListActions, ListProps } from "../../../types";
|
||||
import { ListActions, ListProps } from "../../../types";
|
||||
import { SaleDetails_sale } from "../../types/SaleDetails";
|
||||
import { VoucherDetails_voucher } from "../../types/VoucherDetails";
|
||||
|
||||
export interface SaleProductsProps
|
||||
extends ListProps,
|
||||
ListActions,
|
||||
ChannelProps {
|
||||
export interface SaleProductsProps extends ListProps, ListActions {
|
||||
discount: SaleDetails_sale | VoucherDetails_voucher;
|
||||
channelsCount: number;
|
||||
onProductAssign: () => void;
|
||||
|
@ -83,7 +80,6 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
|
|||
onNextPage,
|
||||
isChecked,
|
||||
selected,
|
||||
selectedChannelId,
|
||||
toggle,
|
||||
toggleAll,
|
||||
toolbar
|
||||
|
@ -162,10 +158,7 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
|
|||
mapEdgesToItems(sale?.products),
|
||||
product => {
|
||||
const isSelected = product ? isChecked(product.id) : false;
|
||||
const channel =
|
||||
product?.channelListings.find(
|
||||
listing => listing.channel.id === selectedChannelId
|
||||
) || product?.channelListings[0];
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
hover={!!product}
|
||||
|
@ -200,7 +193,6 @@ const DiscountProducts: React.FC<SaleProductsProps> = props => {
|
|||
) : product?.channelListings !== undefined ? (
|
||||
<ChannelsAvailabilityDropdown
|
||||
allChannelsCount={channelsCount}
|
||||
currentChannel={channel}
|
||||
channels={product?.channelListings}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -274,7 +274,6 @@ const SaleDetailsPage: React.FC<SaleDetailsPageProps> = ({
|
|||
pageInfo={pageInfo}
|
||||
discount={sale}
|
||||
channelsCount={allChannelsCount}
|
||||
selectedChannelId={selectedChannelId}
|
||||
isChecked={isChecked}
|
||||
selected={selected}
|
||||
toggle={toggle}
|
||||
|
|
|
@ -9,6 +9,7 @@ import TableCellHeader from "@saleor/components/TableCellHeader";
|
|||
import TableHead from "@saleor/components/TableHead";
|
||||
import TablePagination from "@saleor/components/TablePagination";
|
||||
import { SaleListUrlSortField } from "@saleor/discounts/urls";
|
||||
import { canBeSorted } from "@saleor/discounts/views/SaleList/sort";
|
||||
import { maybe, renderCollection } from "@saleor/misc";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import { ChannelProps, ListActions, ListProps, SortPage } from "@saleor/types";
|
||||
|
@ -141,6 +142,9 @@ const SaleList: React.FC<SaleListProps> = props => {
|
|||
}
|
||||
textAlign="right"
|
||||
onClick={() => onSort(SaleListUrlSortField.value)}
|
||||
disabled={
|
||||
!canBeSorted(SaleListUrlSortField.value, !!selectedChannelId)
|
||||
}
|
||||
className={classes.colValue}
|
||||
>
|
||||
<FormattedMessage defaultMessage="Value" description="sale value" />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { IFilter } from "@saleor/components/Filter";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
import { FilterOpts, MinMax } from "@saleor/types";
|
||||
import {
|
||||
DiscountStatusEnum,
|
||||
|
@ -13,13 +14,15 @@ import { defineMessages, IntlShape } from "react-intl";
|
|||
export enum SaleFilterKeys {
|
||||
saleType = "saleType",
|
||||
started = "started",
|
||||
status = "status"
|
||||
status = "status",
|
||||
channel = "channel"
|
||||
}
|
||||
|
||||
export interface SaleListFilterOpts {
|
||||
saleType: FilterOpts<DiscountValueTypeEnum>;
|
||||
started: FilterOpts<MinMax>;
|
||||
status: FilterOpts<DiscountStatusEnum[]>;
|
||||
channel: FilterOpts<string> & { choices: MultiAutocompleteChoiceType[] };
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -27,6 +30,10 @@ const messages = defineMessages({
|
|||
defaultMessage: "Active",
|
||||
description: "sale status"
|
||||
},
|
||||
channel: {
|
||||
defaultMessage: "Channel",
|
||||
description: "sale channel"
|
||||
},
|
||||
expired: {
|
||||
defaultMessage: "Expired",
|
||||
description: "sale status"
|
||||
|
@ -61,6 +68,16 @@ export function createFilterStructure(
|
|||
opts: SaleListFilterOpts
|
||||
): IFilter<SaleFilterKeys> {
|
||||
return [
|
||||
{
|
||||
...createOptionsField(
|
||||
SaleFilterKeys.channel,
|
||||
intl.formatMessage(messages.channel),
|
||||
[opts.channel.value],
|
||||
false,
|
||||
opts.channel.choices
|
||||
),
|
||||
active: opts.channel.active
|
||||
},
|
||||
{
|
||||
...createDateField(
|
||||
SaleFilterKeys.started,
|
||||
|
|
|
@ -346,7 +346,6 @@ const VoucherDetailsPage: React.FC<VoucherDetailsPageProps> = ({
|
|||
onRowClick={onProductClick}
|
||||
pageInfo={pageInfo}
|
||||
discount={voucher}
|
||||
selectedChannelId={selectedChannelId}
|
||||
channelsCount={allChannelsCount}
|
||||
isChecked={isChecked}
|
||||
selected={selected}
|
||||
|
|
|
@ -9,6 +9,7 @@ import TableCellHeader from "@saleor/components/TableCellHeader";
|
|||
import TableHead from "@saleor/components/TableHead";
|
||||
import TablePagination from "@saleor/components/TablePagination";
|
||||
import { VoucherListUrlSortField } from "@saleor/discounts/urls";
|
||||
import { canBeSorted } from "@saleor/discounts/views/VoucherList/sort";
|
||||
import { maybe, renderCollection } from "@saleor/misc";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import { ChannelProps, ListActions, ListProps, SortPage } from "@saleor/types";
|
||||
|
@ -130,6 +131,9 @@ const VoucherList: React.FC<VoucherListProps> = props => {
|
|||
}
|
||||
textAlign="right"
|
||||
onClick={() => onSort(VoucherListUrlSortField.minSpent)}
|
||||
disabled={
|
||||
!canBeSorted(VoucherListUrlSortField.minSpent, !!selectedChannelId)
|
||||
}
|
||||
className={classes.colMinSpent}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -175,6 +179,9 @@ const VoucherList: React.FC<VoucherListProps> = props => {
|
|||
}
|
||||
textAlign="right"
|
||||
onClick={() => onSort(VoucherListUrlSortField.value)}
|
||||
disabled={
|
||||
!canBeSorted(VoucherListUrlSortField.minSpent, !!selectedChannelId)
|
||||
}
|
||||
className={classes.colValue}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { IFilter } from "@saleor/components/Filter";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
import { FilterOpts, MinMax } from "@saleor/types";
|
||||
import {
|
||||
DiscountStatusEnum,
|
||||
|
@ -15,7 +16,8 @@ export enum VoucherFilterKeys {
|
|||
saleType = "saleType",
|
||||
started = "started",
|
||||
status = "status",
|
||||
timesUsed = "timesUsed"
|
||||
timesUsed = "timesUsed",
|
||||
channel = "channel"
|
||||
}
|
||||
|
||||
export interface VoucherListFilterOpts {
|
||||
|
@ -23,6 +25,7 @@ export interface VoucherListFilterOpts {
|
|||
started: FilterOpts<MinMax>;
|
||||
status: FilterOpts<DiscountStatusEnum[]>;
|
||||
timesUsed: FilterOpts<MinMax>;
|
||||
channel: FilterOpts<string> & { choices: MultiAutocompleteChoiceType[] };
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -30,6 +33,10 @@ const messages = defineMessages({
|
|||
defaultMessage: "Active",
|
||||
description: "voucher status"
|
||||
},
|
||||
channel: {
|
||||
defaultMessage: "Channel",
|
||||
description: "voucher channel"
|
||||
},
|
||||
expired: {
|
||||
defaultMessage: "Expired",
|
||||
description: "voucher status"
|
||||
|
@ -68,6 +75,16 @@ export function createFilterStructure(
|
|||
opts: VoucherListFilterOpts
|
||||
): IFilter<VoucherFilterKeys> {
|
||||
return [
|
||||
{
|
||||
...createOptionsField(
|
||||
VoucherFilterKeys.channel,
|
||||
intl.formatMessage(messages.channel),
|
||||
[opts.channel.value],
|
||||
false,
|
||||
opts.channel.choices
|
||||
),
|
||||
active: opts.channel.active
|
||||
},
|
||||
{
|
||||
...createDateField(
|
||||
VoucherFilterKeys.started,
|
||||
|
|
|
@ -26,6 +26,7 @@ export const saleList = gql`
|
|||
$last: Int
|
||||
$filter: SaleFilterInput
|
||||
$sort: SaleSortingInput
|
||||
$channel: String
|
||||
) {
|
||||
sales(
|
||||
after: $after
|
||||
|
@ -34,6 +35,7 @@ export const saleList = gql`
|
|||
last: $last
|
||||
filter: $filter
|
||||
sortBy: $sort
|
||||
channel: $channel
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
|
@ -60,6 +62,7 @@ export const voucherList = gql`
|
|||
$last: Int
|
||||
$filter: VoucherFilterInput
|
||||
$sort: VoucherSortingInput
|
||||
$channel: String
|
||||
) {
|
||||
vouchers(
|
||||
after: $after
|
||||
|
@ -68,6 +71,7 @@ export const voucherList = gql`
|
|||
last: $last
|
||||
filter: $filter
|
||||
sortBy: $sort
|
||||
channel: $channel
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
|
|
|
@ -64,4 +64,5 @@ export interface SaleListVariables {
|
|||
last?: number | null;
|
||||
filter?: SaleFilterInput | null;
|
||||
sort?: SaleSortingInput | null;
|
||||
channel?: string | null;
|
||||
}
|
||||
|
|
|
@ -81,4 +81,5 @@ export interface VoucherListVariables {
|
|||
last?: number | null;
|
||||
filter?: VoucherFilterInput | null;
|
||||
sort?: VoucherSortingInput | null;
|
||||
channel?: string | null;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,8 @@ export enum SaleListUrlFiltersEnum {
|
|||
type = "type",
|
||||
startedFrom = "startedFrom",
|
||||
startedTo = "startedTo",
|
||||
query = "query"
|
||||
query = "query",
|
||||
channel = "channel"
|
||||
}
|
||||
export enum SaleListUrlFiltersWithMultipleValues {
|
||||
status = "status"
|
||||
|
@ -75,7 +76,8 @@ export enum VoucherListUrlFiltersEnum {
|
|||
startedTo = "startedTo",
|
||||
timesUsedFrom = "timesUsedFrom",
|
||||
timesUsedTo = "timesUsedTo",
|
||||
query = "query"
|
||||
query = "query",
|
||||
channel = "channel"
|
||||
}
|
||||
export enum VoucherListUrlFiltersWithMultipleValues {
|
||||
status = "status",
|
||||
|
|
|
@ -20,9 +20,9 @@ import { ListViews } from "@saleor/types";
|
|||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||
import { mapEdgesToItems } from "@saleor/utils/maps";
|
||||
import { mapEdgesToItems, mapNodeToChoice } from "@saleor/utils/maps";
|
||||
import { getSortParams } from "@saleor/utils/sort";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import SaleListPage from "../../components/SaleListPage";
|
||||
|
@ -46,7 +46,7 @@ import {
|
|||
getFilterVariables,
|
||||
saveFilterTab
|
||||
} from "./filters";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
|
||||
|
||||
interface SaleListProps {
|
||||
params: SaleListUrlQueryParams;
|
||||
|
@ -63,7 +63,13 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
ListViews.SALES_LIST
|
||||
);
|
||||
const intl = useIntl();
|
||||
const { channel } = useAppChannel();
|
||||
const { availableChannels } = useAppChannel(false);
|
||||
const selectedChannel = availableChannels.find(
|
||||
channel => channel.slug === params.channel
|
||||
);
|
||||
const channelOpts = availableChannels
|
||||
? mapNodeToChoice(availableChannels, channel => channel.slug)
|
||||
: null;
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
SaleListUrlDialog,
|
||||
|
@ -75,7 +81,8 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
() => ({
|
||||
...paginationState,
|
||||
filter: getFilterVariables(params),
|
||||
sort: getSortQueryVariables(params, channel?.slug)
|
||||
sort: getSortQueryVariables(params),
|
||||
channel: params.channel
|
||||
}),
|
||||
[params]
|
||||
);
|
||||
|
@ -105,6 +112,17 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
params
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!canBeSorted(params.sort, !!selectedChannel)) {
|
||||
navigate(
|
||||
saleListUrl({
|
||||
...params,
|
||||
sort: DEFAULT_SORT_KEY
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
const handleTabChange = (tab: number) => {
|
||||
reset();
|
||||
navigate(
|
||||
|
@ -163,7 +181,7 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
<WindowTitle title={intl.formatMessage(sectionNames.sales)} />
|
||||
<SaleListPage
|
||||
currentTab={currentTab}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
filterOpts={getFilterOpts(params, channelOpts)}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={filter => changeFilters(filter)}
|
||||
|
@ -199,7 +217,7 @@ export const SaleList: React.FC<SaleListProps> = ({ params }) => {
|
|||
<DeleteIcon />
|
||||
</IconButton>
|
||||
}
|
||||
selectedChannelId={channel?.id}
|
||||
selectedChannelId={selectedChannel?.id}
|
||||
/>
|
||||
<ActionDialog
|
||||
confirmButtonState={saleBulkDeleteOpts.status}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||
Object {
|
||||
"channel": "default-channel",
|
||||
"startedFrom": "2019-12-09",
|
||||
"startedTo": "2019-12-38",
|
||||
"status": Array [
|
||||
|
@ -12,4 +13,4 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"startedFrom=2019-12-09&startedTo=2019-12-38&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type=FIXED"`;
|
||||
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"channel=default-channel&startedFrom=2019-12-09&startedTo=2019-12-38&status%5B0%5D=ACTIVE&status%5B1%5D=EXPIRED&type=FIXED"`;
|
||||
|
|
|
@ -38,6 +38,16 @@ describe("Filtering URL params", () => {
|
|||
const intl = createIntl(config);
|
||||
|
||||
const filters = createFilterStructure(intl, {
|
||||
channel: {
|
||||
active: false,
|
||||
choices: [
|
||||
{
|
||||
value: "default-channel",
|
||||
label: "Default channel"
|
||||
}
|
||||
],
|
||||
value: "default-channel"
|
||||
},
|
||||
saleType: {
|
||||
active: false,
|
||||
value: DiscountValueTypeEnum.FIXED
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { IFilterElement } from "@saleor/components/Filter";
|
||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import {
|
||||
SaleFilterKeys,
|
||||
SaleListFilterOpts
|
||||
|
@ -17,7 +18,8 @@ import {
|
|||
getGteLteVariables,
|
||||
getMinMaxQueryParam,
|
||||
getMultipleEnumValueQueryParam,
|
||||
getSingleEnumValueQueryParam
|
||||
getSingleEnumValueQueryParam,
|
||||
getSingleValueQueryParam
|
||||
} from "../../../utils/filters";
|
||||
import {
|
||||
SaleListUrlFilters,
|
||||
|
@ -28,8 +30,16 @@ import {
|
|||
|
||||
export const SALE_FILTERS_KEY = "saleFilters";
|
||||
|
||||
export function getFilterOpts(params: SaleListUrlFilters): SaleListFilterOpts {
|
||||
export function getFilterOpts(
|
||||
params: SaleListUrlFilters,
|
||||
channels: SingleAutocompleteChoiceType[]
|
||||
): SaleListFilterOpts {
|
||||
return {
|
||||
channel: {
|
||||
active: params?.channel !== undefined,
|
||||
choices: channels,
|
||||
value: params?.channel
|
||||
},
|
||||
saleType: {
|
||||
active: !!maybe(() => params.type),
|
||||
value: maybe(() => findValueInEnum(params.type, DiscountValueTypeEnum))
|
||||
|
@ -105,6 +115,9 @@ export function getFilterQueryParam(
|
|||
SaleListUrlFiltersWithMultipleValues.status,
|
||||
DiscountStatusEnum
|
||||
);
|
||||
|
||||
case SaleFilterKeys.channel:
|
||||
return getSingleValueQueryParam(filter, SaleListUrlFiltersEnum.channel);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,25 @@ import { SaleListUrlSortField } from "@saleor/discounts/urls";
|
|||
import { SaleSortField } from "@saleor/types/globalTypes";
|
||||
import { createGetSortQueryVariables } from "@saleor/utils/sort";
|
||||
|
||||
export const DEFAULT_SORT_KEY = SaleListUrlSortField.name;
|
||||
|
||||
export function canBeSorted(
|
||||
sort: SaleListUrlSortField,
|
||||
isChannelSelected: boolean
|
||||
) {
|
||||
switch (sort) {
|
||||
case SaleListUrlSortField.name:
|
||||
case SaleListUrlSortField.startDate:
|
||||
case SaleListUrlSortField.endDate:
|
||||
case SaleListUrlSortField.type:
|
||||
return true;
|
||||
case SaleListUrlSortField.value:
|
||||
return isChannelSelected;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSortQueryField(sort: SaleListUrlSortField): SaleSortField {
|
||||
switch (sort) {
|
||||
case SaleListUrlSortField.name:
|
||||
|
|
|
@ -20,9 +20,9 @@ import { ListViews } from "@saleor/types";
|
|||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||
import createSortHandler from "@saleor/utils/handlers/sortHandler";
|
||||
import { mapEdgesToItems } from "@saleor/utils/maps";
|
||||
import { mapEdgesToItems, mapNodeToChoice } from "@saleor/utils/maps";
|
||||
import { getSortParams } from "@saleor/utils/sort";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import VoucherListPage from "../../components/VoucherListPage";
|
||||
|
@ -46,7 +46,7 @@ import {
|
|||
getFilterVariables,
|
||||
saveFilterTab
|
||||
} from "./filters";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
|
||||
|
||||
interface VoucherListProps {
|
||||
params: VoucherListUrlQueryParams;
|
||||
|
@ -64,7 +64,13 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
|||
);
|
||||
const intl = useIntl();
|
||||
|
||||
const { channel } = useAppChannel();
|
||||
const { availableChannels } = useAppChannel(false);
|
||||
const selectedChannel = availableChannels.find(
|
||||
channel => channel.slug === params.channel
|
||||
);
|
||||
const channelOpts = availableChannels
|
||||
? mapNodeToChoice(availableChannels, channel => channel.slug)
|
||||
: null;
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
VoucherListUrlDialog,
|
||||
|
@ -76,7 +82,8 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
|||
() => ({
|
||||
...paginationState,
|
||||
filter: getFilterVariables(params),
|
||||
sort: getSortQueryVariables(params, channel?.slug)
|
||||
sort: getSortQueryVariables(params),
|
||||
channel: params.channel
|
||||
}),
|
||||
[params]
|
||||
);
|
||||
|
@ -106,6 +113,17 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
|||
params
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!canBeSorted(params.sort, !!selectedChannel)) {
|
||||
navigate(
|
||||
voucherListUrl({
|
||||
...params,
|
||||
sort: DEFAULT_SORT_KEY
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
const handleTabChange = (tab: number) => {
|
||||
reset();
|
||||
navigate(
|
||||
|
@ -164,7 +182,7 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
|||
<WindowTitle title={intl.formatMessage(sectionNames.vouchers)} />
|
||||
<VoucherListPage
|
||||
currentTab={currentTab}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
filterOpts={getFilterOpts(params, channelOpts)}
|
||||
initialSearch={params.query || ""}
|
||||
onSearchChange={handleSearchChange}
|
||||
onFilterChange={filter => changeFilters(filter)}
|
||||
|
@ -200,7 +218,7 @@ export const VoucherList: React.FC<VoucherListProps> = ({ params }) => {
|
|||
<DeleteIcon />
|
||||
</IconButton>
|
||||
}
|
||||
selectedChannelId={channel?.id}
|
||||
selectedChannelId={selectedChannel?.id}
|
||||
/>
|
||||
<ActionDialog
|
||||
confirmButtonState={voucherBulkDeleteOpts.status}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||
Object {
|
||||
"channel": "default-channel",
|
||||
"startedFrom": "2019-12-09",
|
||||
"startedTo": "2019-12-38",
|
||||
"status": Array [
|
||||
|
@ -17,4 +18,4 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"startedFrom=2019-12-09&startedTo=2019-12-38×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 filters = createFilterStructure(intl, {
|
||||
channel: {
|
||||
active: false,
|
||||
choices: [
|
||||
{
|
||||
value: "default-channel",
|
||||
label: "Default channel"
|
||||
}
|
||||
],
|
||||
value: "default-channel"
|
||||
},
|
||||
saleType: {
|
||||
active: false,
|
||||
value: [VoucherDiscountType.FIXED, VoucherDiscountType.SHIPPING]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { IFilterElement } from "@saleor/components/Filter";
|
||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import {
|
||||
VoucherFilterKeys,
|
||||
VoucherListFilterOpts
|
||||
|
@ -16,7 +17,8 @@ import {
|
|||
dedupeFilter,
|
||||
getGteLteVariables,
|
||||
getMinMaxQueryParam,
|
||||
getMultipleEnumValueQueryParam
|
||||
getMultipleEnumValueQueryParam,
|
||||
getSingleValueQueryParam
|
||||
} from "../../../utils/filters";
|
||||
import {
|
||||
VoucherListUrlFilters,
|
||||
|
@ -28,9 +30,15 @@ import {
|
|||
export const VOUCHER_FILTERS_KEY = "voucherFilters";
|
||||
|
||||
export function getFilterOpts(
|
||||
params: VoucherListUrlFilters
|
||||
params: VoucherListUrlFilters,
|
||||
channels: SingleAutocompleteChoiceType[]
|
||||
): VoucherListFilterOpts {
|
||||
return {
|
||||
channel: {
|
||||
active: params?.channel !== undefined,
|
||||
choices: channels,
|
||||
value: params?.channel
|
||||
},
|
||||
saleType: {
|
||||
active: !!maybe(() => params.type),
|
||||
value: maybe(
|
||||
|
@ -137,6 +145,12 @@ export function getFilterQueryParam(
|
|||
VoucherListUrlFiltersWithMultipleValues.status,
|
||||
DiscountStatusEnum
|
||||
);
|
||||
|
||||
case VoucherFilterKeys.channel:
|
||||
return getSingleValueQueryParam(
|
||||
filter,
|
||||
VoucherListUrlFiltersEnum.channel
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,27 @@ import { VoucherListUrlSortField } from "@saleor/discounts/urls";
|
|||
import { VoucherSortField } from "@saleor/types/globalTypes";
|
||||
import { createGetSortQueryVariables } from "@saleor/utils/sort";
|
||||
|
||||
export const DEFAULT_SORT_KEY = VoucherListUrlSortField.code;
|
||||
|
||||
export function canBeSorted(
|
||||
sort: VoucherListUrlSortField,
|
||||
isChannelSelected: boolean
|
||||
) {
|
||||
switch (sort) {
|
||||
case VoucherListUrlSortField.code:
|
||||
case VoucherListUrlSortField.startDate:
|
||||
case VoucherListUrlSortField.endDate:
|
||||
case VoucherListUrlSortField.type:
|
||||
case VoucherListUrlSortField.limit:
|
||||
return true;
|
||||
case VoucherListUrlSortField.value:
|
||||
case VoucherListUrlSortField.minSpent:
|
||||
return isChannelSelected;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSortQueryField(
|
||||
sort: VoucherListUrlSortField
|
||||
): VoucherSortField {
|
||||
|
|
|
@ -34,7 +34,8 @@ const home = gql`
|
|||
totalCount
|
||||
}
|
||||
productsOutOfStock: products(
|
||||
filter: { stockAvailability: OUT_OF_STOCK, channel: $channel }
|
||||
filter: { stockAvailability: OUT_OF_STOCK }
|
||||
channel: $channel
|
||||
) {
|
||||
totalCount
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import urlJoin from "url-join";
|
|||
|
||||
import { ConfirmButtonTransitionState } from "./components/ConfirmButton/ConfirmButton";
|
||||
import { StatusType } from "./components/StatusChip/types";
|
||||
import { StatusLabelProps } from "./components/StatusLabel";
|
||||
import { APP_MOUNT_URI } from "./config";
|
||||
import { AddressType, AddressTypeInput } from "./customers/types";
|
||||
import {
|
||||
|
@ -90,7 +91,10 @@ const paymentStatusMessages = defineMessages({
|
|||
}
|
||||
});
|
||||
|
||||
export const transformPaymentStatus = (status: string, intl: IntlShape) => {
|
||||
export const transformPaymentStatus = (
|
||||
status: string,
|
||||
intl: IntlShape
|
||||
): { localized: string; status: StatusLabelProps["status"] } => {
|
||||
switch (status) {
|
||||
case PaymentChargeStatusEnum.PARTIALLY_CHARGED:
|
||||
return {
|
||||
|
|
|
@ -2,8 +2,8 @@ import { defineMessages } from "react-intl";
|
|||
|
||||
export const pluginsFilterErrorMessages = defineMessages({
|
||||
active: {
|
||||
defaultMessage: "Active is not selected",
|
||||
description: "plugin filters error messages active"
|
||||
defaultMessage: "Status is not selected",
|
||||
description: "plugin filters error messages status"
|
||||
},
|
||||
channels: {
|
||||
defaultMessage: "No channels selected",
|
||||
|
|
|
@ -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 { ProductList_products_edges_node } from "@saleor/products/types/ProductList";
|
||||
import { ProductListUrlSortField } from "@saleor/products/urls";
|
||||
import { canBeSorted } from "@saleor/products/views/ProductList/sort";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import { ChannelProps, ListActions, ListProps, SortPage } from "@saleor/types";
|
||||
import TDisplayColumn, {
|
||||
|
@ -34,6 +35,8 @@ import classNames from "classnames";
|
|||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import ProductAvailabilityStatusLabel from "../ProductAvailabilityStatusLabel";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
[theme.breakpoints.up("lg")]: {
|
||||
|
@ -215,6 +218,12 @@ export const ProductList: React.FC<ProductListProps> = props => {
|
|||
: undefined
|
||||
}
|
||||
onClick={() => onSort(ProductListUrlSortField.status)}
|
||||
disabled={
|
||||
!canBeSorted(
|
||||
ProductListUrlSortField.status,
|
||||
!!selectedChannelId
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Availability"
|
||||
|
@ -262,6 +271,9 @@ export const ProductList: React.FC<ProductListProps> = props => {
|
|||
}
|
||||
textAlign="right"
|
||||
onClick={() => onSort(ProductListUrlSortField.price)}
|
||||
disabled={
|
||||
!canBeSorted(ProductListUrlSortField.price, !!selectedChannelId)
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Price"
|
||||
|
@ -361,17 +373,17 @@ export const ProductList: React.FC<ProductListProps> = props => {
|
|||
!!product?.channelListings?.length
|
||||
}
|
||||
>
|
||||
{product && !product?.channelListings?.length ? (
|
||||
"-"
|
||||
) : product?.channelListings !== undefined ? (
|
||||
<ChannelsAvailabilityDropdown
|
||||
allChannelsCount={channelsCount}
|
||||
currentChannel={channel}
|
||||
channels={product?.channelListings}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
{(!product && <Skeleton />) ||
|
||||
(!product?.channelListings?.length && "-") ||
|
||||
(product?.channelListings !== undefined && channel ? (
|
||||
<ProductAvailabilityStatusLabel channel={channel} />
|
||||
) : (
|
||||
<ChannelsAvailabilityDropdown
|
||||
allChannelsCount={channelsCount}
|
||||
channels={product?.channelListings}
|
||||
showStatus
|
||||
/>
|
||||
))}
|
||||
</TableCell>
|
||||
</DisplayColumn>
|
||||
{gridAttributesFromSettings.map(gridAttribute => (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { IFilter } from "@saleor/components/Filter";
|
||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import { commonMessages, sectionNames } from "@saleor/intl";
|
||||
import { AutocompleteFilterOpts, FilterOpts, MinMax } from "@saleor/types";
|
||||
import {
|
||||
|
@ -19,7 +20,8 @@ export enum ProductFilterKeys {
|
|||
collections = "collections",
|
||||
price = "price",
|
||||
productType = "productType",
|
||||
stock = "stock"
|
||||
stock = "stock",
|
||||
channel = "channel"
|
||||
}
|
||||
|
||||
export interface ProductListFilterOpts {
|
||||
|
@ -37,6 +39,7 @@ export interface ProductListFilterOpts {
|
|||
price: FilterOpts<MinMax>;
|
||||
productType: FilterOpts<string[]> & AutocompleteFilterOpts;
|
||||
stockStatus: FilterOpts<StockAvailability>;
|
||||
channel: FilterOpts<string> & { choices: SingleAutocompleteChoiceType[] };
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -44,6 +47,10 @@ const messages = defineMessages({
|
|||
defaultMessage: "Available",
|
||||
description: "product status"
|
||||
},
|
||||
channel: {
|
||||
defaultMessage: "Channel",
|
||||
description: "sales channel"
|
||||
},
|
||||
hidden: {
|
||||
defaultMessage: "Hidden",
|
||||
description: "product is hidden"
|
||||
|
@ -81,6 +88,16 @@ export function createFilterStructure(
|
|||
);
|
||||
|
||||
return [
|
||||
{
|
||||
...createOptionsField(
|
||||
ProductFilterKeys.channel,
|
||||
intl.formatMessage(messages.channel),
|
||||
[opts.channel.value],
|
||||
false,
|
||||
opts.channel.choices
|
||||
),
|
||||
active: opts.channel.active
|
||||
},
|
||||
{
|
||||
...createOptionsField(
|
||||
ProductFilterKeys.stock,
|
||||
|
@ -98,7 +115,8 @@ export function createFilterStructure(
|
|||
}
|
||||
]
|
||||
),
|
||||
active: opts.stockStatus.active
|
||||
active: opts.stockStatus.active,
|
||||
dependencies: [ProductFilterKeys.channel]
|
||||
},
|
||||
{
|
||||
...createPriceField(
|
||||
|
|
|
@ -139,6 +139,7 @@ const productListQuery = gql`
|
|||
$last: Int
|
||||
$before: String
|
||||
$filter: ProductFilterInput
|
||||
$channel: String
|
||||
$sort: ProductOrder
|
||||
) {
|
||||
products(
|
||||
|
@ -148,6 +149,7 @@ const productListQuery = gql`
|
|||
last: $last
|
||||
filter: $filter
|
||||
sortBy: $sort
|
||||
channel: $channel
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
|
@ -177,8 +179,8 @@ export const useProductListQuery = makeQuery<ProductList, ProductListVariables>(
|
|||
);
|
||||
|
||||
const productCountQuery = gql`
|
||||
query ProductCount($filter: ProductFilterInput) {
|
||||
products(filter: $filter) {
|
||||
query ProductCount($filter: ProductFilterInput, $channel: String) {
|
||||
products(filter: $filter, channel: $channel) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,4 +20,5 @@ export interface ProductCount {
|
|||
|
||||
export interface ProductCountVariables {
|
||||
filter?: ProductFilterInput | null;
|
||||
channel?: string | null;
|
||||
}
|
||||
|
|
|
@ -140,5 +140,6 @@ export interface ProductListVariables {
|
|||
last?: number | null;
|
||||
before?: string | null;
|
||||
filter?: ProductFilterInput | null;
|
||||
channel?: string | null;
|
||||
sort?: ProductOrder | null;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,8 @@ export enum ProductListUrlFiltersEnum {
|
|||
priceTo = "priceTo",
|
||||
status = "status",
|
||||
stockStatus = "stockStatus",
|
||||
query = "query"
|
||||
query = "query",
|
||||
channel = "channel"
|
||||
}
|
||||
export enum ProductListUrlFiltersWithMultipleValues {
|
||||
categories = "categories",
|
||||
|
|
|
@ -55,7 +55,7 @@ import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
|
|||
import { ListViews } from "@saleor/types";
|
||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||
import { mapEdgesToItems } from "@saleor/utils/maps";
|
||||
import { mapEdgesToItems, mapNodeToChoice } from "@saleor/utils/maps";
|
||||
import { getSortUrlVariables } from "@saleor/utils/sort";
|
||||
import { useWarehouseList } from "@saleor/warehouses/queries";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
@ -76,7 +76,7 @@ import {
|
|||
getFilterVariables,
|
||||
saveFilterTab
|
||||
} from "./filters";
|
||||
import { getSortQueryVariables } from "./sort";
|
||||
import { canBeSorted, DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
|
||||
|
||||
interface ProductListProps {
|
||||
params: ProductListUrlQueryParams;
|
||||
|
@ -160,14 +160,16 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
},
|
||||
skip: params.action !== "export"
|
||||
});
|
||||
const { availableChannels, channel } = useAppChannel();
|
||||
const { availableChannels } = useAppChannel(false);
|
||||
const limitOpts = useShopLimitsQuery({
|
||||
variables: {
|
||||
productVariants: true
|
||||
}
|
||||
});
|
||||
|
||||
const noChannel = !channel && typeof channel !== "undefined";
|
||||
const selectedChannel = availableChannels.find(
|
||||
channel => channel.slug === params.channel
|
||||
);
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
ProductListUrlDialog,
|
||||
|
@ -238,7 +240,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
const sortWithQuery = ProductListUrlSortField.rank;
|
||||
const sortWithoutQuery =
|
||||
params.sort === ProductListUrlSortField.rank
|
||||
? ProductListUrlSortField.name
|
||||
? DEFAULT_SORT_KEY
|
||||
: params.sort;
|
||||
navigate(
|
||||
productListUrl({
|
||||
|
@ -249,6 +251,17 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
);
|
||||
}, [params.query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canBeSorted(params.sort, !!selectedChannel)) {
|
||||
navigate(
|
||||
productListUrl({
|
||||
...params,
|
||||
sort: DEFAULT_SORT_KEY
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
const handleTabChange = (tab: number) => {
|
||||
reset();
|
||||
navigate(
|
||||
|
@ -281,17 +294,21 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
);
|
||||
|
||||
const paginationState = createPaginationState(settings.rowNumber, params);
|
||||
const channelSlug = noChannel ? null : channel.slug;
|
||||
const filter = getFilterVariables(params, channelSlug);
|
||||
const sort = getSortQueryVariables(params, channelSlug);
|
||||
const channelOpts = availableChannels
|
||||
? mapNodeToChoice(availableChannels, channel => channel.slug)
|
||||
: null;
|
||||
const filter = getFilterVariables(params, !!selectedChannel);
|
||||
const sort = getSortQueryVariables(params, !!selectedChannel);
|
||||
const queryVariables = React.useMemo<ProductListVariables>(
|
||||
() => ({
|
||||
...paginationState,
|
||||
filter,
|
||||
sort
|
||||
sort,
|
||||
channel: selectedChannel?.slug
|
||||
}),
|
||||
[params, settings.rowNumber]
|
||||
);
|
||||
// TODO: When channel is undefined we should skip detailed pricing listings
|
||||
const { data, loading, refetch } = useProductListQuery({
|
||||
displayLoader: true,
|
||||
variables: queryVariables
|
||||
|
@ -339,7 +356,8 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
{
|
||||
initial: mapEdgesToItems(initialFilterProductTypes?.productTypes),
|
||||
search: searchProductTypes
|
||||
}
|
||||
},
|
||||
channelOpts
|
||||
);
|
||||
|
||||
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
|
||||
|
@ -360,7 +378,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
availableInGridAttributes={mapEdgesToItems(
|
||||
attributes?.data?.availableInGrid
|
||||
)}
|
||||
currencySymbol={channel?.currencyCode || ""}
|
||||
currencySymbol={selectedChannel?.currencyCode || ""}
|
||||
currentTab={currentTab}
|
||||
defaultSettings={defaultListSettings[ListViews.PRODUCT_LIST]}
|
||||
filterOpts={filterOpts}
|
||||
|
@ -437,7 +455,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
|||
tabs={getFilterTabs().map(tab => tab.name)}
|
||||
onExport={() => openModal("export")}
|
||||
channelsCount={availableChannels?.length}
|
||||
selectedChannelId={channel?.id}
|
||||
selectedChannelId={selectedChannel?.id}
|
||||
/>
|
||||
<ActionDialog
|
||||
open={params.action === "delete"}
|
||||
|
|
|
@ -55,6 +55,7 @@ Object {
|
|||
"categories": Array [
|
||||
"878752",
|
||||
],
|
||||
"channel": "default-channel",
|
||||
"collections": Array [
|
||||
"Q29sbGVjdGlvbjoc",
|
||||
],
|
||||
|
@ -67,4 +68,4 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"stockStatus=IN_STOCK&priceFrom=10&priceTo=20&categories%5B0%5D=878752&collections%5B0%5D=Q29sbGVjdGlvbjoc&productTypes%5B0%5D=UHJvZHVjdFR5cGU6MQ%3D%3D&attributes%5Bauthor%5D%5B0%5D=john-doe&attributes%5Bauthor%5D%5B1%5D=false&attributes%5Bbox-size%5D%5B0%5D=100g&attributes%5Bbox-size%5D%5B1%5D=500g&attributes%5Bbrand%5D%5B0%5D=saleor&attributes%5Bbrand%5D%5B1%5D=false&attributes%5Bcandy-box-size%5D%5B0%5D=100g&attributes%5Bcandy-box-size%5D%5B1%5D=500g&attributes%5Bcoffee-genre%5D%5B0%5D=arabica&attributes%5Bcoffee-genre%5D%5B1%5D=false&attributes%5Bcollar%5D%5B0%5D=round&attributes%5Bcollar%5D%5B1%5D=polo&attributes%5Bcolor%5D%5B0%5D=blue&attributes%5Bcolor%5D%5B1%5D=false&attributes%5Bcover%5D%5B0%5D=soft&attributes%5Bcover%5D%5B1%5D=middle-soft&attributes%5Bflavor%5D%5B0%5D=sour&attributes%5Bflavor%5D%5B1%5D=false&attributes%5Blanguage%5D%5B0%5D=english&attributes%5Blanguage%5D%5B1%5D=false&attributes%5Bpublisher%5D%5B0%5D=mirumee-press&attributes%5Bpublisher%5D%5B1%5D=false&attributes%5Bsize%5D%5B0%5D=xs&attributes%5Bsize%5D%5B1%5D=m"`;
|
||||
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"channel=default-channel&stockStatus=IN_STOCK&priceFrom=10&priceTo=20&categories%5B0%5D=878752&collections%5B0%5D=Q29sbGVjdGlvbjoc&productTypes%5B0%5D=UHJvZHVjdFR5cGU6MQ%3D%3D&attributes%5Bauthor%5D%5B0%5D=john-doe&attributes%5Bauthor%5D%5B1%5D=false&attributes%5Bbox-size%5D%5B0%5D=100g&attributes%5Bbox-size%5D%5B1%5D=500g&attributes%5Bbrand%5D%5B0%5D=saleor&attributes%5Bbrand%5D%5B1%5D=false&attributes%5Bcandy-box-size%5D%5B0%5D=100g&attributes%5Bcandy-box-size%5D%5B1%5D=500g&attributes%5Bcoffee-genre%5D%5B0%5D=arabica&attributes%5Bcoffee-genre%5D%5B1%5D=false&attributes%5Bcollar%5D%5B0%5D=round&attributes%5Bcollar%5D%5B1%5D=polo&attributes%5Bcolor%5D%5B0%5D=blue&attributes%5Bcolor%5D%5B1%5D=false&attributes%5Bcover%5D%5B0%5D=soft&attributes%5Bcover%5D%5B1%5D=middle-soft&attributes%5Bflavor%5D%5B0%5D=sour&attributes%5Bflavor%5D%5B1%5D=false&attributes%5Blanguage%5D%5B0%5D=english&attributes%5Blanguage%5D%5B1%5D=false&attributes%5Bpublisher%5D%5B0%5D=mirumee-press&attributes%5Bpublisher%5D%5B1%5D=false&attributes%5Bsize%5D%5B0%5D=xs&attributes%5Bsize%5D%5B1%5D=m"`;
|
||||
|
|
|
@ -25,9 +25,9 @@ describe("Filtering query params", () => {
|
|||
status: true.toString(),
|
||||
stockStatus: StockAvailability.IN_STOCK
|
||||
};
|
||||
const filterVariables = getFilterVariables(params, "default-channel");
|
||||
const filterVariables = getFilterVariables(params, true);
|
||||
|
||||
expect(getExistingKeys(filterVariables)).toHaveLength(3);
|
||||
expect(getExistingKeys(filterVariables)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import { UseSearchResult } from "@saleor/hooks/makeSearch";
|
||||
import { findValueInEnum, maybe } from "@saleor/misc";
|
||||
import {
|
||||
|
@ -43,7 +44,8 @@ import {
|
|||
getGteLteVariables,
|
||||
getMinMaxQueryParam,
|
||||
getMultipleValueQueryParam,
|
||||
getSingleEnumValueQueryParam
|
||||
getSingleEnumValueQueryParam,
|
||||
getSingleValueQueryParam
|
||||
} from "../../../utils/filters";
|
||||
import {
|
||||
ProductListUrlFilters,
|
||||
|
@ -73,7 +75,8 @@ export function getFilterOpts(
|
|||
productTypes: {
|
||||
initial: InitialProductFilterProductTypes_productTypes_edges_node[];
|
||||
search: UseSearchResult<SearchProductTypes, SearchProductTypesVariables>;
|
||||
}
|
||||
},
|
||||
channels: SingleAutocompleteChoiceType[]
|
||||
): ProductListFilterOpts {
|
||||
return {
|
||||
attributes: attributes
|
||||
|
@ -131,6 +134,11 @@ export function getFilterOpts(
|
|||
onSearchChange: categories.search.search,
|
||||
value: maybe(() => dedupeFilter(params.categories), [])
|
||||
},
|
||||
channel: {
|
||||
active: params?.channel !== undefined,
|
||||
choices: channels,
|
||||
value: params?.channel
|
||||
},
|
||||
collections: {
|
||||
active: !!params.collections,
|
||||
choices: mapNodeToChoice(
|
||||
|
@ -224,14 +232,13 @@ function getFilteredAttributeValue(
|
|||
|
||||
export function getFilterVariables(
|
||||
params: ProductListUrlFilters,
|
||||
channel: string | undefined
|
||||
isChannelSelected: boolean
|
||||
): ProductFilterInput {
|
||||
return {
|
||||
attributes: getFilteredAttributeValue(params),
|
||||
categories: params.categories !== undefined ? params.categories : null,
|
||||
channel: channel || null,
|
||||
collections: params.collections !== undefined ? params.collections : null,
|
||||
price: channel
|
||||
price: isChannelSelected
|
||||
? getGteLteVariables({
|
||||
gte: parseFloat(params.priceFrom),
|
||||
lte: parseFloat(params.priceTo)
|
||||
|
@ -298,6 +305,12 @@ export function getFilterQueryParam(
|
|||
ProductListUrlFiltersEnum.stockStatus,
|
||||
StockAvailability
|
||||
);
|
||||
|
||||
case ProductFilterKeys.channel:
|
||||
return getSingleValueQueryParam(
|
||||
filter,
|
||||
ProductListUrlFiltersEnum.channel
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,16 @@ export const productListFilterOpts: ProductListFilterOpts = {
|
|||
],
|
||||
value: [categories[5].id]
|
||||
},
|
||||
channel: {
|
||||
active: false,
|
||||
value: "default-channel",
|
||||
choices: [
|
||||
{
|
||||
value: "default-channel",
|
||||
label: "Default channel"
|
||||
}
|
||||
]
|
||||
},
|
||||
collections: {
|
||||
...fetchMoreProps,
|
||||
...searchPageProps,
|
||||
|
|
|
@ -5,6 +5,26 @@ import {
|
|||
import { ProductOrder, ProductOrderField } from "@saleor/types/globalTypes";
|
||||
import { getOrderDirection } from "@saleor/utils/sort";
|
||||
|
||||
export const DEFAULT_SORT_KEY = ProductListUrlSortField.name;
|
||||
|
||||
export function canBeSorted(
|
||||
sort: ProductListUrlSortField,
|
||||
isChannelSelected: boolean
|
||||
) {
|
||||
switch (sort) {
|
||||
case ProductListUrlSortField.name:
|
||||
case ProductListUrlSortField.productType:
|
||||
case ProductListUrlSortField.attribute:
|
||||
case ProductListUrlSortField.rank:
|
||||
return true;
|
||||
case ProductListUrlSortField.price:
|
||||
case ProductListUrlSortField.status:
|
||||
return isChannelSelected;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSortQueryField(
|
||||
sort: ProductListUrlSortField
|
||||
): ProductOrderField {
|
||||
|
@ -26,8 +46,12 @@ export function getSortQueryField(
|
|||
|
||||
export function getSortQueryVariables(
|
||||
params: ProductListUrlQueryParams,
|
||||
channel: string
|
||||
isChannelSelected: boolean
|
||||
): ProductOrder {
|
||||
if (!canBeSorted(params.sort, isChannelSelected)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const direction = getOrderDirection(params.asc);
|
||||
if (params.sort === ProductListUrlSortField.attribute) {
|
||||
return {
|
||||
|
@ -35,9 +59,9 @@ export function getSortQueryVariables(
|
|||
direction
|
||||
};
|
||||
}
|
||||
|
||||
const field = getSortQueryField(params.sort);
|
||||
return {
|
||||
channel,
|
||||
direction,
|
||||
field
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -26,6 +26,16 @@ const props: SaleListPageProps = {
|
|||
...sortPageProps,
|
||||
...tabPageProps,
|
||||
filterOpts: {
|
||||
channel: {
|
||||
active: false,
|
||||
value: "default-channel",
|
||||
choices: [
|
||||
{
|
||||
value: "default-channel",
|
||||
label: "Default channel"
|
||||
}
|
||||
]
|
||||
},
|
||||
saleType: {
|
||||
active: false,
|
||||
value: DiscountValueTypeEnum.FIXED
|
||||
|
|
|
@ -28,6 +28,16 @@ const props: VoucherListPageProps = {
|
|||
...tabPageProps,
|
||||
...filterPageProps,
|
||||
filterOpts: {
|
||||
channel: {
|
||||
active: false,
|
||||
value: "default-channel",
|
||||
choices: [
|
||||
{
|
||||
value: "default-channel",
|
||||
label: "Default channel"
|
||||
}
|
||||
]
|
||||
},
|
||||
saleType: {
|
||||
active: false,
|
||||
value: [VoucherDiscountType.FIXED, VoucherDiscountType.PERCENTAGE]
|
||||
|
|
|
@ -1118,13 +1118,12 @@ export interface AttributeFilterInput {
|
|||
filterableInStorefront?: boolean | null;
|
||||
filterableInDashboard?: boolean | null;
|
||||
availableInGrid?: boolean | null;
|
||||
metadata?: (MetadataInput | null)[] | null;
|
||||
metadata?: (MetadataFilter | null)[] | null;
|
||||
search?: string | null;
|
||||
ids?: (string | null)[] | null;
|
||||
type?: AttributeTypeEnum | null;
|
||||
inCollection?: string | null;
|
||||
inCategory?: string | null;
|
||||
channel?: string | null;
|
||||
}
|
||||
|
||||
export interface AttributeInput {
|
||||
|
@ -1188,7 +1187,7 @@ export interface CatalogueInput {
|
|||
|
||||
export interface CategoryFilterInput {
|
||||
search?: string | null;
|
||||
metadata?: (MetadataInput | null)[] | null;
|
||||
metadata?: (MetadataFilter | null)[] | null;
|
||||
ids?: (string | null)[] | null;
|
||||
}
|
||||
|
||||
|
@ -1203,7 +1202,6 @@ export interface CategoryInput {
|
|||
|
||||
export interface CategorySortingInput {
|
||||
direction: OrderDirection;
|
||||
channel?: string | null;
|
||||
field: CategorySortField;
|
||||
}
|
||||
|
||||
|
@ -1247,9 +1245,8 @@ export interface CollectionCreateInput {
|
|||
export interface CollectionFilterInput {
|
||||
published?: CollectionPublished | null;
|
||||
search?: string | null;
|
||||
metadata?: (MetadataInput | null)[] | null;
|
||||
metadata?: (MetadataFilter | null)[] | null;
|
||||
ids?: (string | null)[] | null;
|
||||
channel?: string | null;
|
||||
}
|
||||
|
||||
export interface CollectionInput {
|
||||
|
@ -1265,7 +1262,6 @@ export interface CollectionInput {
|
|||
|
||||
export interface CollectionSortingInput {
|
||||
direction: OrderDirection;
|
||||
channel?: string | null;
|
||||
field: CollectionSortField;
|
||||
}
|
||||
|
||||
|
@ -1279,7 +1275,7 @@ export interface CustomerFilterInput {
|
|||
numberOfOrders?: IntRangeInput | null;
|
||||
placedOrders?: DateRangeInput | null;
|
||||
search?: string | null;
|
||||
metadata?: (MetadataInput | null)[] | null;
|
||||
metadata?: (MetadataFilter | null)[] | null;
|
||||
}
|
||||
|
||||
export interface CustomerInput {
|
||||
|
@ -1394,6 +1390,11 @@ export interface MenuSortingInput {
|
|||
field: MenuSortField;
|
||||
}
|
||||
|
||||
export interface MetadataFilter {
|
||||
key: string;
|
||||
value?: string | null;
|
||||
}
|
||||
|
||||
export interface MetadataInput {
|
||||
key: string;
|
||||
value: string;
|
||||
|
@ -1417,7 +1418,7 @@ export interface OrderDraftFilterInput {
|
|||
customer?: string | null;
|
||||
created?: DateRangeInput | null;
|
||||
search?: string | null;
|
||||
metadata?: (MetadataInput | null)[] | null;
|
||||
metadata?: (MetadataFilter | null)[] | null;
|
||||
channels?: (string | null)[] | null;
|
||||
}
|
||||
|
||||
|
@ -1427,7 +1428,7 @@ export interface OrderFilterInput {
|
|||
customer?: string | null;
|
||||
created?: DateRangeInput | null;
|
||||
search?: string | null;
|
||||
metadata?: (MetadataInput | null)[] | null;
|
||||
metadata?: (MetadataFilter | null)[] | null;
|
||||
channels?: (string | null)[] | null;
|
||||
}
|
||||
|
||||
|
@ -1524,7 +1525,7 @@ export interface PageCreateInput {
|
|||
|
||||
export interface PageFilterInput {
|
||||
search?: string | null;
|
||||
metadata?: (MetadataInput | null)[] | null;
|
||||
metadata?: (MetadataFilter | null)[] | null;
|
||||
pageTypes?: (string | null)[] | null;
|
||||
ids?: (string | null)[] | null;
|
||||
}
|
||||
|
@ -1667,12 +1668,11 @@ export interface ProductFilterInput {
|
|||
stockAvailability?: StockAvailability | null;
|
||||
stocks?: ProductStockFilterInput | null;
|
||||
search?: string | null;
|
||||
metadata?: (MetadataInput | null)[] | null;
|
||||
metadata?: (MetadataFilter | null)[] | null;
|
||||
price?: PriceRangeInput | null;
|
||||
minimalPrice?: PriceRangeInput | null;
|
||||
productTypes?: (string | null)[] | null;
|
||||
ids?: (string | null)[] | null;
|
||||
channel?: string | null;
|
||||
}
|
||||
|
||||
export interface ProductInput {
|
||||
|
@ -1691,7 +1691,6 @@ export interface ProductInput {
|
|||
|
||||
export interface ProductOrder {
|
||||
direction: OrderDirection;
|
||||
channel?: string | null;
|
||||
attributeId?: string | null;
|
||||
field?: ProductOrderField | null;
|
||||
}
|
||||
|
@ -1705,7 +1704,7 @@ export interface ProductTypeFilterInput {
|
|||
search?: string | null;
|
||||
configurable?: ProductTypeConfigurable | null;
|
||||
productType?: ProductTypeEnum | null;
|
||||
metadata?: (MetadataInput | null)[] | null;
|
||||
metadata?: (MetadataFilter | null)[] | null;
|
||||
ids?: (string | null)[] | null;
|
||||
}
|
||||
|
||||
|
@ -1798,7 +1797,6 @@ export interface SaleInput {
|
|||
|
||||
export interface SaleSortingInput {
|
||||
direction: OrderDirection;
|
||||
channel?: string | null;
|
||||
field: SaleSortField;
|
||||
}
|
||||
|
||||
|
@ -1987,7 +1985,6 @@ export interface VoucherInput {
|
|||
|
||||
export interface VoucherSortingInput {
|
||||
direction: OrderDirection;
|
||||
channel?: string | null;
|
||||
field: VoucherSortField;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo";
|
||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import {
|
||||
ChoiceValue,
|
||||
SingleAutocompleteChoiceType
|
||||
} from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import { MetadataItem } from "@saleor/fragments/types/MetadataItem";
|
||||
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
|
||||
import { Node, SlugNode } from "@saleor/types";
|
||||
|
@ -18,48 +20,46 @@ export function mapEdgesToItems<T>(data?: EdgesType<T>): T[] {
|
|||
return data.edges.map(({ node }) => node);
|
||||
}
|
||||
|
||||
export function mapCountriesToChoices(
|
||||
countries: ShopInfo_shop_countries[]
|
||||
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
|
||||
export function mapCountriesToChoices(countries: ShopInfo_shop_countries[]) {
|
||||
return countries.map(country => ({
|
||||
label: country.country,
|
||||
value: country.code
|
||||
}));
|
||||
}
|
||||
|
||||
export function mapPagesToChoices(
|
||||
pages: SearchPages_search_edges_node[]
|
||||
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
|
||||
export function mapPagesToChoices(pages: SearchPages_search_edges_node[]) {
|
||||
return pages.map(page => ({
|
||||
label: page.title,
|
||||
value: page.id
|
||||
}));
|
||||
}
|
||||
|
||||
export function mapNodeToChoice(
|
||||
nodes: Array<Node & Record<"name", string>>
|
||||
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
|
||||
type ExtendedNode = Node & Record<"name", string>;
|
||||
export function mapNodeToChoice<T extends ExtendedNode>(
|
||||
nodes: T[]
|
||||
): Array<SingleAutocompleteChoiceType<string>>;
|
||||
export function mapNodeToChoice<T extends ExtendedNode, K extends ChoiceValue>(
|
||||
nodes: T[],
|
||||
getterFn: (node: T) => K
|
||||
): Array<SingleAutocompleteChoiceType<K>>;
|
||||
export function mapNodeToChoice<T extends ExtendedNode>(
|
||||
nodes: T[],
|
||||
getterFn?: (node: T) => any
|
||||
) {
|
||||
if (!nodes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return nodes.map(node => ({
|
||||
label: node.name,
|
||||
value: node.id
|
||||
value: getterFn ? getterFn(node) : node.id
|
||||
}));
|
||||
}
|
||||
|
||||
export function mapSlugNodeToChoice(
|
||||
nodes: Array<SlugNode & Record<"name", string>>
|
||||
): Array<SingleAutocompleteChoiceType | MultiAutocompleteChoiceType> {
|
||||
if (!nodes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return nodes.map(node => ({
|
||||
label: node.name,
|
||||
value: node.slug
|
||||
}));
|
||||
nodes: Array<ExtendedNode & SlugNode>
|
||||
): SingleAutocompleteChoiceType[] {
|
||||
return mapNodeToChoice(nodes, nodes => nodes.slug);
|
||||
}
|
||||
|
||||
export function mapMetadataItemToInput(item: MetadataItem): MetadataInput {
|
||||
|
|
|
@ -72,7 +72,7 @@ type GetSortQueryField<TUrlField extends string, TSortField extends string> = (
|
|||
type GetSortQueryVariables<
|
||||
TSortField extends string,
|
||||
TParams extends Record<any, any>
|
||||
> = (params: TParams, channelSlug?: string) => SortingInput<TSortField>;
|
||||
> = (params: TParams) => SortingInput<TSortField>;
|
||||
export function createGetSortQueryVariables<
|
||||
TUrlField extends string,
|
||||
TSortField extends string,
|
||||
|
@ -80,14 +80,13 @@ export function createGetSortQueryVariables<
|
|||
>(
|
||||
getSortQueryField: GetSortQueryField<TUrlField, TSortField>
|
||||
): GetSortQueryVariables<TSortField, TParams> {
|
||||
return (params: TParams, channelSlug?: string) => {
|
||||
return (params: TParams) => {
|
||||
const field = getSortQueryField(params.sort);
|
||||
|
||||
if (!!field) {
|
||||
return {
|
||||
direction: getOrderDirection(params.asc),
|
||||
field,
|
||||
channel: channelSlug
|
||||
field
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue