Saleor 3087 Paginate attribute values in filters (#1152)
* Dynamic fetch attribute values in filter list * Update filter attributes fixtures * Change attribute values filter to autocomplete field * Fix unchecking attribute value filter failure * Update test snapshots * Update changelog * Fix cypress tests * Add slug node mapping util
This commit is contained in:
parent
99aa6365be
commit
c3e720a47e
20 changed files with 2145 additions and 941 deletions
|
@ -48,6 +48,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
||||||
- Choosing user shipping and billing addresses for draft order - #1082 by @orzechdev
|
- Choosing user shipping and billing addresses for draft order - #1082 by @orzechdev
|
||||||
- Fix EditorJS inline formatting - #1096 by @orzechdev
|
- Fix EditorJS inline formatting - #1096 by @orzechdev
|
||||||
- Add pagination on attribute values - #1112 by @orzechdev
|
- Add pagination on attribute values - #1112 by @orzechdev
|
||||||
|
- Paginate attribute values in filters - #1152 by @orzechdev
|
||||||
- Fix attribute values input display - #1156 by @orzechdev
|
- Fix attribute values input display - #1156 by @orzechdev
|
||||||
|
|
||||||
# 2.11.1
|
# 2.11.1
|
||||||
|
|
|
@ -21,10 +21,11 @@ export const PRODUCTS_LIST = {
|
||||||
filterOption: '[data-test-id="filterOption"]',
|
filterOption: '[data-test-id="filterOption"]',
|
||||||
productsOutOfStockOption: '[data-test-id="OUT_OF_STOCK"]',
|
productsOutOfStockOption: '[data-test-id="OUT_OF_STOCK"]',
|
||||||
filterBy: {
|
filterBy: {
|
||||||
category: '[data-test-id="categories"]',
|
category: '[data-test="filterGroupActive"][data-test-id="categories"]',
|
||||||
collection: '[data-test-id="collections"]',
|
collection: '[data-test="filterGroupActive"][data-test-id="collections"]',
|
||||||
productType: '[data-test-id="productType"]',
|
productType:
|
||||||
stock: '[data-test-id="stock"]'
|
'[data-test="filterGroupActive"][data-test-id="productType"]',
|
||||||
|
stock: '[data-test="filterGroupActive"][data-test-id="stock"]'
|
||||||
},
|
},
|
||||||
filterBySearchInput: '[data-test*="filterField"][data-test*="Input"]'
|
filterBySearchInput: '[data-test*="filterField"][data-test*="Input"]'
|
||||||
},
|
},
|
||||||
|
|
|
@ -1666,6 +1666,7 @@ input CustomerFilterInput {
|
||||||
numberOfOrders: IntRangeInput
|
numberOfOrders: IntRangeInput
|
||||||
placedOrders: DateRangeInput
|
placedOrders: DateRangeInput
|
||||||
search: String
|
search: String
|
||||||
|
metadata: [MetadataInput]
|
||||||
}
|
}
|
||||||
|
|
||||||
input CustomerInput {
|
input CustomerInput {
|
||||||
|
@ -6132,10 +6133,11 @@ enum WebhookSampleEventTypeEnum {
|
||||||
PAGE_DELETED
|
PAGE_DELETED
|
||||||
PAYMENT_AUTHORIZE
|
PAYMENT_AUTHORIZE
|
||||||
PAYMENT_CAPTURE
|
PAYMENT_CAPTURE
|
||||||
|
PAYMENT_CONFIRM
|
||||||
|
PAYMENT_LIST_GATEWAYS
|
||||||
|
PAYMENT_PROCESS
|
||||||
PAYMENT_REFUND
|
PAYMENT_REFUND
|
||||||
PAYMENT_VOID
|
PAYMENT_VOID
|
||||||
PAYMENT_CONFIRM
|
|
||||||
PAYMENT_PROCESS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebhookUpdate {
|
type WebhookUpdate {
|
||||||
|
|
|
@ -21,6 +21,7 @@ export interface FilterProps<TFilterKeys extends string = string> {
|
||||||
errorMessages?: FilterErrorMessages<TFilterKeys>;
|
errorMessages?: FilterErrorMessages<TFilterKeys>;
|
||||||
menu: IFilter<TFilterKeys>;
|
menu: IFilter<TFilterKeys>;
|
||||||
onFilterAdd: (filter: Array<IFilterElement<string>>) => void;
|
onFilterAdd: (filter: Array<IFilterElement<string>>) => void;
|
||||||
|
onFilterAttributeFocus?: (id?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(
|
const useStyles = makeStyles(
|
||||||
|
@ -91,7 +92,13 @@ const useStyles = makeStyles(
|
||||||
{ name: "Filter" }
|
{ name: "Filter" }
|
||||||
);
|
);
|
||||||
const Filter: React.FC<FilterProps> = props => {
|
const Filter: React.FC<FilterProps> = props => {
|
||||||
const { currencySymbol, menu, onFilterAdd, errorMessages } = props;
|
const {
|
||||||
|
currencySymbol,
|
||||||
|
menu,
|
||||||
|
onFilterAdd,
|
||||||
|
onFilterAttributeFocus,
|
||||||
|
errorMessages
|
||||||
|
} = props;
|
||||||
const classes = useStyles(props);
|
const classes = useStyles(props);
|
||||||
|
|
||||||
const anchor = React.useRef<HTMLDivElement>();
|
const anchor = React.useRef<HTMLDivElement>();
|
||||||
|
@ -190,6 +197,7 @@ const Filter: React.FC<FilterProps> = props => {
|
||||||
filters={data}
|
filters={data}
|
||||||
onClear={reset}
|
onClear={reset}
|
||||||
onFilterPropertyChange={dispatch}
|
onFilterPropertyChange={dispatch}
|
||||||
|
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
</Grow>
|
</Grow>
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import { Paper, Typography } from "@material-ui/core";
|
import {
|
||||||
|
ExpansionPanel,
|
||||||
|
ExpansionPanelSummary,
|
||||||
|
makeStyles,
|
||||||
|
Paper,
|
||||||
|
Typography
|
||||||
|
} from "@material-ui/core";
|
||||||
import CollectionWithDividers from "@saleor/components/CollectionWithDividers";
|
import CollectionWithDividers from "@saleor/components/CollectionWithDividers";
|
||||||
import Hr from "@saleor/components/Hr";
|
import Hr from "@saleor/components/Hr";
|
||||||
import useStateFromProps from "@saleor/hooks/useStateFromProps";
|
import useStateFromProps from "@saleor/hooks/useStateFromProps";
|
||||||
import React from "react";
|
import IconChevronDown from "@saleor/icons/ChevronDown";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { FilterAutocompleteDisplayValues } from "../FilterAutocompleteField";
|
import { FilterAutocompleteDisplayValues } from "../FilterAutocompleteField";
|
||||||
import { FilterReducerAction } from "../reducer";
|
import { FilterReducerAction } from "../reducer";
|
||||||
|
@ -18,9 +25,57 @@ import FilterContentBodyNameField from "./FilterContentBodyNameField";
|
||||||
import FilterContentHeader from "./FilterContentHeader";
|
import FilterContentHeader from "./FilterContentHeader";
|
||||||
import FilterErrorsList from "./FilterErrorsList";
|
import FilterErrorsList from "./FilterErrorsList";
|
||||||
|
|
||||||
|
const useExpanderStyles = makeStyles(
|
||||||
|
() => ({
|
||||||
|
expanded: {},
|
||||||
|
root: {
|
||||||
|
boxShadow: "none",
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
|
||||||
|
"&:before": {
|
||||||
|
content: "none"
|
||||||
|
},
|
||||||
|
|
||||||
|
"&$expanded": {
|
||||||
|
margin: 0,
|
||||||
|
border: "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ name: "FilterContentExpander" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const useSummaryStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
expanded: {},
|
||||||
|
root: {
|
||||||
|
width: "100%",
|
||||||
|
border: "none",
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
minHeight: 0,
|
||||||
|
paddingRight: theme.spacing(2),
|
||||||
|
|
||||||
|
"&$expanded": {
|
||||||
|
minHeight: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
margin: 0,
|
||||||
|
|
||||||
|
"&$expanded": {
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ name: "FilterContentExpanderSummary" }
|
||||||
|
);
|
||||||
|
|
||||||
export interface FilterContentProps<T extends string = string> {
|
export interface FilterContentProps<T extends string = string> {
|
||||||
filters: IFilter<T>;
|
filters: IFilter<T>;
|
||||||
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
||||||
|
onFilterAttributeFocus?: (id?: string) => void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
currencySymbol?: string;
|
currencySymbol?: string;
|
||||||
|
@ -36,9 +91,15 @@ const FilterContent: React.FC<FilterContentProps> = ({
|
||||||
filters,
|
filters,
|
||||||
onClear,
|
onClear,
|
||||||
onFilterPropertyChange,
|
onFilterPropertyChange,
|
||||||
|
onFilterAttributeFocus,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
dataStructure
|
dataStructure
|
||||||
}) => {
|
}) => {
|
||||||
|
const expanderClasses = useExpanderStyles({});
|
||||||
|
const summaryClasses = useSummaryStyles({});
|
||||||
|
|
||||||
|
const [openedFilter, setOpenedFilter] = useState<IFilterElement<string>>();
|
||||||
|
|
||||||
const getAutocompleteValuesWithNewValues = (
|
const getAutocompleteValuesWithNewValues = (
|
||||||
autocompleteDisplayValues: FilterAutocompleteDisplayValues,
|
autocompleteDisplayValues: FilterAutocompleteDisplayValues,
|
||||||
filterField: IFilterElement<string>
|
filterField: IFilterElement<string>
|
||||||
|
@ -84,6 +145,36 @@ const FilterContent: React.FC<FilterContentProps> = ({
|
||||||
initialAutocompleteDisplayValues
|
initialAutocompleteDisplayValues
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFilterAttributeFocus = (filter?: IFilterElement<string>) => {
|
||||||
|
setOpenedFilter(filter);
|
||||||
|
if (onFilterAttributeFocus) {
|
||||||
|
onFilterAttributeFocus(filter?.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterOpen = (filter: IFilterElement<string>) => {
|
||||||
|
if (filter.name !== openedFilter?.name) {
|
||||||
|
handleFilterAttributeFocus(filter);
|
||||||
|
} else {
|
||||||
|
handleFilterAttributeFocus(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterPropertyGroupChange = function<T extends string>(
|
||||||
|
action: FilterReducerAction<T>,
|
||||||
|
filter: IFilterElement<string>
|
||||||
|
) {
|
||||||
|
const switchToActive = action.payload.update.active;
|
||||||
|
|
||||||
|
if (switchToActive && filter.name !== openedFilter?.name) {
|
||||||
|
handleFilterAttributeFocus(filter);
|
||||||
|
} else if (!switchToActive && filter.name === openedFilter?.name) {
|
||||||
|
handleFilterAttributeFocus(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterPropertyChange(action);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMultipleFieldPropertyChange = function<T extends string>(
|
const handleMultipleFieldPropertyChange = function<T extends string>(
|
||||||
action: FilterReducerAction<T>
|
action: FilterReducerAction<T>
|
||||||
) {
|
) {
|
||||||
|
@ -114,11 +205,24 @@ const FilterContent: React.FC<FilterContentProps> = ({
|
||||||
{dataStructure
|
{dataStructure
|
||||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||||
.map(filter => (
|
.map(filter => (
|
||||||
<React.Fragment key={filter.name}>
|
<ExpansionPanel
|
||||||
<FilterContentBodyNameField
|
key={filter.name}
|
||||||
filter={getFilterFromCurrentData(filter)}
|
classes={expanderClasses}
|
||||||
onFilterPropertyChange={onFilterPropertyChange}
|
data-test="channel-availability-item"
|
||||||
/>
|
expanded={filter.name === openedFilter?.name}
|
||||||
|
>
|
||||||
|
<ExpansionPanelSummary
|
||||||
|
expandIcon={<IconChevronDown />}
|
||||||
|
classes={summaryClasses}
|
||||||
|
onClick={() => handleFilterOpen(filter)}
|
||||||
|
>
|
||||||
|
<FilterContentBodyNameField
|
||||||
|
filter={getFilterFromCurrentData(filter)}
|
||||||
|
onFilterPropertyChange={action =>
|
||||||
|
handleFilterPropertyGroupChange(action, filter)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ExpansionPanelSummary>
|
||||||
<FilterErrorsList
|
<FilterErrorsList
|
||||||
errors={errors}
|
errors={errors}
|
||||||
errorMessages={errorMessages}
|
errorMessages={errorMessages}
|
||||||
|
@ -147,7 +251,7 @@ const FilterContent: React.FC<FilterContentProps> = ({
|
||||||
filter={getFilterFromCurrentData(filter)}
|
filter={getFilterFromCurrentData(filter)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</ExpansionPanel>
|
||||||
))}
|
))}
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
|
@ -74,10 +74,6 @@ const FilterContentBody: React.FC<FilterContentBodyProps> = ({
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const classes = useStyles({});
|
const classes = useStyles({});
|
||||||
|
|
||||||
if (!filter?.active) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.filterSettings}>
|
<div className={classes.filterSettings}>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -43,6 +43,7 @@ const FilterContentBodyNameField: React.FC<FilterContentBodyNameFieldProps> = ({
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={filter.label}
|
label={filter.label}
|
||||||
|
onClick={event => event.stopPropagation()}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
onFilterPropertyChange({
|
onFilterPropertyChange({
|
||||||
payload: {
|
payload: {
|
||||||
|
|
|
@ -32,6 +32,7 @@ export interface IFilterElement<T extends string = string>
|
||||||
type?: FieldType;
|
type?: FieldType;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
multipleFields?: IFilterElement[];
|
multipleFields?: IFilterElement[];
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilterBaseFieldProps<T extends string = string> {
|
export interface FilterBaseFieldProps<T extends string = string> {
|
||||||
|
|
|
@ -48,6 +48,7 @@ const FilterBar: React.FC<FilterBarProps> = props => {
|
||||||
onAll,
|
onAll,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
|
onFilterAttributeFocus,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
onTabSave,
|
onTabSave,
|
||||||
|
@ -90,6 +91,7 @@ const FilterBar: React.FC<FilterBarProps> = props => {
|
||||||
menu={filterStructure}
|
menu={filterStructure}
|
||||||
currencySymbol={currencySymbol}
|
currencySymbol={currencySymbol}
|
||||||
onFilterAdd={onFilterChange}
|
onFilterAdd={onFilterChange}
|
||||||
|
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||||
/>
|
/>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
|
|
|
@ -87,6 +87,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
onExport,
|
onExport,
|
||||||
onFetchMore,
|
onFetchMore,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
|
onFilterAttributeFocus,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onTabDelete,
|
onTabDelete,
|
||||||
|
@ -196,6 +197,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
initialSearch={initialSearch}
|
initialSearch={initialSearch}
|
||||||
onAll={onAll}
|
onAll={onAll}
|
||||||
onFilterChange={onFilterChange}
|
onFilterChange={onFilterChange}
|
||||||
|
onFilterAttributeFocus={onFilterAttributeFocus}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
onTabChange={onTabChange}
|
onTabChange={onTabChange}
|
||||||
onTabDelete={onTabDelete}
|
onTabDelete={onTabDelete}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { IFilter } from "@saleor/components/Filter";
|
import { IFilter } from "@saleor/components/Filter";
|
||||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
|
||||||
import { sectionNames } from "@saleor/intl";
|
import { sectionNames } from "@saleor/intl";
|
||||||
import { AutocompleteFilterOpts, FilterOpts, MinMax } from "@saleor/types";
|
import { AutocompleteFilterOpts, FilterOpts, MinMax } from "@saleor/types";
|
||||||
import { StockAvailability } from "@saleor/types/globalTypes";
|
import { StockAvailability } from "@saleor/types/globalTypes";
|
||||||
|
@ -22,11 +21,12 @@ export enum ProductFilterKeys {
|
||||||
export interface ProductListFilterOpts {
|
export interface ProductListFilterOpts {
|
||||||
attributes: Array<
|
attributes: Array<
|
||||||
FilterOpts<string[]> & {
|
FilterOpts<string[]> & {
|
||||||
choices: MultiAutocompleteChoiceType[];
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
attributeChoices: FilterOpts<string[]> & AutocompleteFilterOpts;
|
||||||
categories: FilterOpts<string[]> & AutocompleteFilterOpts;
|
categories: FilterOpts<string[]> & AutocompleteFilterOpts;
|
||||||
collections: FilterOpts<string[]> & AutocompleteFilterOpts;
|
collections: FilterOpts<string[]> & AutocompleteFilterOpts;
|
||||||
price: FilterOpts<MinMax>;
|
price: FilterOpts<MinMax>;
|
||||||
|
@ -151,12 +151,21 @@ export function createFilterStructure(
|
||||||
active: opts.productType.active
|
active: opts.productType.active
|
||||||
},
|
},
|
||||||
...opts.attributes.map(attr => ({
|
...opts.attributes.map(attr => ({
|
||||||
...createOptionsField(
|
...createAutocompleteField(
|
||||||
attr.slug as any,
|
attr.slug as any,
|
||||||
attr.name,
|
attr.name,
|
||||||
attr.value,
|
attr.value,
|
||||||
|
opts.attributeChoices.displayValues,
|
||||||
true,
|
true,
|
||||||
attr.choices
|
opts.attributeChoices.choices,
|
||||||
|
{
|
||||||
|
hasMore: opts.attributeChoices.hasMore,
|
||||||
|
initialSearch: "",
|
||||||
|
loading: opts.attributeChoices.loading,
|
||||||
|
onFetchMore: opts.attributeChoices.onFetchMore,
|
||||||
|
onSearchChange: opts.attributeChoices.onSearchChange
|
||||||
|
},
|
||||||
|
attr.id
|
||||||
),
|
),
|
||||||
active: attr.active,
|
active: attr.active,
|
||||||
group: ProductFilterKeys.attributes
|
group: ProductFilterKeys.attributes
|
||||||
|
|
|
@ -27,10 +27,7 @@ import {
|
||||||
GridAttributes,
|
GridAttributes,
|
||||||
GridAttributesVariables
|
GridAttributesVariables
|
||||||
} from "./types/GridAttributes";
|
} from "./types/GridAttributes";
|
||||||
import {
|
import { InitialProductFilterAttributes } from "./types/InitialProductFilterAttributes";
|
||||||
InitialProductFilterAttributes,
|
|
||||||
InitialProductFilterAttributesVariables
|
|
||||||
} from "./types/InitialProductFilterAttributes";
|
|
||||||
import {
|
import {
|
||||||
InitialProductFilterCategories,
|
InitialProductFilterCategories,
|
||||||
InitialProductFilterCategoriesVariables
|
InitialProductFilterCategoriesVariables
|
||||||
|
@ -60,13 +57,7 @@ import {
|
||||||
} from "./types/ProductVariantDetails";
|
} from "./types/ProductVariantDetails";
|
||||||
|
|
||||||
const initialProductFilterAttributesQuery = gql`
|
const initialProductFilterAttributesQuery = gql`
|
||||||
${pageInfoFragment}
|
query InitialProductFilterAttributes {
|
||||||
query InitialProductFilterAttributes(
|
|
||||||
$firstValues: Int
|
|
||||||
$afterValues: String
|
|
||||||
$lastValues: Int
|
|
||||||
$beforeValues: String
|
|
||||||
) {
|
|
||||||
attributes(
|
attributes(
|
||||||
first: 100
|
first: 100
|
||||||
filter: { filterableInDashboard: true, type: PRODUCT_TYPE }
|
filter: { filterableInDashboard: true, type: PRODUCT_TYPE }
|
||||||
|
@ -76,24 +67,6 @@ const initialProductFilterAttributesQuery = gql`
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
choices(
|
|
||||||
first: $firstValues
|
|
||||||
after: $afterValues
|
|
||||||
last: $lastValues
|
|
||||||
before: $beforeValues
|
|
||||||
) {
|
|
||||||
pageInfo {
|
|
||||||
...PageInfoFragment
|
|
||||||
}
|
|
||||||
edges {
|
|
||||||
cursor
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,7 +74,7 @@ const initialProductFilterAttributesQuery = gql`
|
||||||
`;
|
`;
|
||||||
export const useInitialProductFilterAttributesQuery = makeQuery<
|
export const useInitialProductFilterAttributesQuery = makeQuery<
|
||||||
InitialProductFilterAttributes,
|
InitialProductFilterAttributes,
|
||||||
InitialProductFilterAttributesVariables
|
never
|
||||||
>(initialProductFilterAttributesQuery);
|
>(initialProductFilterAttributesQuery);
|
||||||
|
|
||||||
const initialProductFilterCategoriesQuery = gql`
|
const initialProductFilterCategoriesQuery = gql`
|
||||||
|
|
|
@ -7,39 +7,11 @@
|
||||||
// GraphQL query operation: InitialProductFilterAttributes
|
// GraphQL query operation: InitialProductFilterAttributes
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
export interface InitialProductFilterAttributes_attributes_edges_node_choices_pageInfo {
|
|
||||||
__typename: "PageInfo";
|
|
||||||
endCursor: string | null;
|
|
||||||
hasNextPage: boolean;
|
|
||||||
hasPreviousPage: boolean;
|
|
||||||
startCursor: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InitialProductFilterAttributes_attributes_edges_node_choices_edges_node {
|
|
||||||
__typename: "AttributeValue";
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
slug: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InitialProductFilterAttributes_attributes_edges_node_choices_edges {
|
|
||||||
__typename: "AttributeValueCountableEdge";
|
|
||||||
cursor: string;
|
|
||||||
node: InitialProductFilterAttributes_attributes_edges_node_choices_edges_node;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InitialProductFilterAttributes_attributes_edges_node_choices {
|
|
||||||
__typename: "AttributeValueCountableConnection";
|
|
||||||
pageInfo: InitialProductFilterAttributes_attributes_edges_node_choices_pageInfo;
|
|
||||||
edges: InitialProductFilterAttributes_attributes_edges_node_choices_edges[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InitialProductFilterAttributes_attributes_edges_node {
|
export interface InitialProductFilterAttributes_attributes_edges_node {
|
||||||
__typename: "Attribute";
|
__typename: "Attribute";
|
||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
slug: string | null;
|
slug: string | null;
|
||||||
choices: InitialProductFilterAttributes_attributes_edges_node_choices | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitialProductFilterAttributes_attributes_edges {
|
export interface InitialProductFilterAttributes_attributes_edges {
|
||||||
|
@ -55,10 +27,3 @@ export interface InitialProductFilterAttributes_attributes {
|
||||||
export interface InitialProductFilterAttributes {
|
export interface InitialProductFilterAttributes {
|
||||||
attributes: InitialProductFilterAttributes_attributes | null;
|
attributes: InitialProductFilterAttributes_attributes | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitialProductFilterAttributesVariables {
|
|
||||||
firstValues?: number | null;
|
|
||||||
afterValues?: string | null;
|
|
||||||
lastValues?: number | null;
|
|
||||||
beforeValues?: string | null;
|
|
||||||
}
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ import {
|
||||||
productUrl
|
productUrl
|
||||||
} from "@saleor/products/urls";
|
} from "@saleor/products/urls";
|
||||||
import useAttributeSearch from "@saleor/searches/useAttributeSearch";
|
import useAttributeSearch from "@saleor/searches/useAttributeSearch";
|
||||||
|
import useAttributeValueSearch from "@saleor/searches/useAttributeValueSearch";
|
||||||
import useCategorySearch from "@saleor/searches/useCategorySearch";
|
import useCategorySearch from "@saleor/searches/useCategorySearch";
|
||||||
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
|
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
|
||||||
import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
|
import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
|
||||||
|
@ -57,7 +58,7 @@ import createFilterHandlers from "@saleor/utils/handlers/filterHandlers";
|
||||||
import { mapEdgesToItems } from "@saleor/utils/maps";
|
import { mapEdgesToItems } 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 } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import ProductListPage from "../../components/ProductListPage";
|
import ProductListPage from "../../components/ProductListPage";
|
||||||
|
@ -95,11 +96,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
data: initialFilterAttributes
|
data: initialFilterAttributes
|
||||||
} = useInitialProductFilterAttributesQuery({
|
} = useInitialProductFilterAttributesQuery({});
|
||||||
variables: {
|
|
||||||
firstValues: 100
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const {
|
const {
|
||||||
data: initialFilterCategories
|
data: initialFilterCategories
|
||||||
} = useInitialProductFilterCategoriesQuery({
|
} = useInitialProductFilterCategoriesQuery({
|
||||||
|
@ -148,6 +145,15 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
first: 10
|
first: 10
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const [focusedAttribute, setFocusedAttribute] = useState<string>();
|
||||||
|
const searchAttributeValues = useAttributeValueSearch({
|
||||||
|
variables: {
|
||||||
|
id: focusedAttribute,
|
||||||
|
...DEFAULT_INITIAL_SEARCH_DATA,
|
||||||
|
first: 10
|
||||||
|
},
|
||||||
|
skip: !focusedAttribute
|
||||||
|
});
|
||||||
const warehouses = useWarehouseList({
|
const warehouses = useWarehouseList({
|
||||||
variables: {
|
variables: {
|
||||||
first: 100
|
first: 100
|
||||||
|
@ -321,6 +327,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
const filterOpts = getFilterOpts(
|
const filterOpts = getFilterOpts(
|
||||||
params,
|
params,
|
||||||
mapEdgesToItems(initialFilterAttributes?.attributes),
|
mapEdgesToItems(initialFilterAttributes?.attributes),
|
||||||
|
searchAttributeValues,
|
||||||
{
|
{
|
||||||
initial: mapEdgesToItems(initialFilterCategories?.categories),
|
initial: mapEdgesToItems(initialFilterCategories?.categories),
|
||||||
search: searchCategories
|
search: searchCategories
|
||||||
|
@ -422,6 +429,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
|
||||||
toggleAll={toggleAll}
|
toggleAll={toggleAll}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
onFilterChange={changeFilters}
|
onFilterChange={changeFilters}
|
||||||
|
onFilterAttributeFocus={setFocusedAttribute}
|
||||||
onTabSave={() => openModal("save-search")}
|
onTabSave={() => openModal("save-search")}
|
||||||
onTabDelete={() => openModal("delete-search")}
|
onTabDelete={() => openModal("delete-search")}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
|
|
|
@ -8,6 +8,10 @@ import { InitialProductFilterAttributes_attributes_edges_node } from "@saleor/pr
|
||||||
import { InitialProductFilterCategories_categories_edges_node } from "@saleor/products/types/InitialProductFilterCategories";
|
import { InitialProductFilterCategories_categories_edges_node } from "@saleor/products/types/InitialProductFilterCategories";
|
||||||
import { InitialProductFilterCollections_collections_edges_node } from "@saleor/products/types/InitialProductFilterCollections";
|
import { InitialProductFilterCollections_collections_edges_node } from "@saleor/products/types/InitialProductFilterCollections";
|
||||||
import { InitialProductFilterProductTypes_productTypes_edges_node } from "@saleor/products/types/InitialProductFilterProductTypes";
|
import { InitialProductFilterProductTypes_productTypes_edges_node } from "@saleor/products/types/InitialProductFilterProductTypes";
|
||||||
|
import {
|
||||||
|
SearchAttributeValues,
|
||||||
|
SearchAttributeValuesVariables
|
||||||
|
} from "@saleor/searches/types/SearchAttributeValues";
|
||||||
import {
|
import {
|
||||||
SearchCategories,
|
SearchCategories,
|
||||||
SearchCategoriesVariables
|
SearchCategoriesVariables
|
||||||
|
@ -20,7 +24,11 @@ import {
|
||||||
SearchProductTypes,
|
SearchProductTypes,
|
||||||
SearchProductTypesVariables
|
SearchProductTypesVariables
|
||||||
} from "@saleor/searches/types/SearchProductTypes";
|
} from "@saleor/searches/types/SearchProductTypes";
|
||||||
import { mapEdgesToItems, mapNodeToChoice } from "@saleor/utils/maps";
|
import {
|
||||||
|
mapEdgesToItems,
|
||||||
|
mapNodeToChoice,
|
||||||
|
mapSlugNodeToChoice
|
||||||
|
} from "@saleor/utils/maps";
|
||||||
import isArray from "lodash/isArray";
|
import isArray from "lodash/isArray";
|
||||||
|
|
||||||
import { IFilterElement } from "../../../components/Filter";
|
import { IFilterElement } from "../../../components/Filter";
|
||||||
|
@ -50,6 +58,10 @@ export const PRODUCT_FILTERS_KEY = "productFilters";
|
||||||
export function getFilterOpts(
|
export function getFilterOpts(
|
||||||
params: ProductListUrlFilters,
|
params: ProductListUrlFilters,
|
||||||
attributes: InitialProductFilterAttributes_attributes_edges_node[],
|
attributes: InitialProductFilterAttributes_attributes_edges_node[],
|
||||||
|
focusedAttributeChoices: UseSearchResult<
|
||||||
|
SearchAttributeValues,
|
||||||
|
SearchAttributeValuesVariables
|
||||||
|
>,
|
||||||
categories: {
|
categories: {
|
||||||
initial: InitialProductFilterCategories_categories_edges_node[];
|
initial: InitialProductFilterCategories_categories_edges_node[];
|
||||||
search: UseSearchResult<SearchCategories, SearchCategoriesVariables>;
|
search: UseSearchResult<SearchCategories, SearchCategoriesVariables>;
|
||||||
|
@ -68,17 +80,31 @@ export function getFilterOpts(
|
||||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||||
.map(attr => ({
|
.map(attr => ({
|
||||||
active: maybe(() => params.attributes[attr.slug].length > 0, false),
|
active: maybe(() => params.attributes[attr.slug].length > 0, false),
|
||||||
choices: attr.choices?.edges?.map(val => ({
|
id: attr.id,
|
||||||
label: val.node.name,
|
|
||||||
value: val.node.slug
|
|
||||||
})),
|
|
||||||
name: attr.name,
|
name: attr.name,
|
||||||
slug: attr.slug,
|
slug: attr.slug,
|
||||||
value:
|
value:
|
||||||
!!params.attributes && params.attributes[attr.slug]
|
!!params.attributes && params.attributes[attr.slug]
|
||||||
? params.attributes[attr.slug]
|
? dedupeFilter(params.attributes[attr.slug])
|
||||||
: []
|
: []
|
||||||
})),
|
})),
|
||||||
|
attributeChoices: {
|
||||||
|
active: true,
|
||||||
|
choices: mapSlugNodeToChoice(
|
||||||
|
mapEdgesToItems(focusedAttributeChoices.result.data?.attribute?.choices)
|
||||||
|
),
|
||||||
|
displayValues: mapNodeToChoice(
|
||||||
|
mapEdgesToItems(focusedAttributeChoices.result.data?.attribute?.choices)
|
||||||
|
),
|
||||||
|
hasMore:
|
||||||
|
focusedAttributeChoices.result.data?.attribute?.choices?.pageInfo
|
||||||
|
?.hasNextPage || false,
|
||||||
|
initialSearch: "",
|
||||||
|
loading: focusedAttributeChoices.result.loading,
|
||||||
|
onFetchMore: focusedAttributeChoices.loadMore,
|
||||||
|
onSearchChange: focusedAttributeChoices.search,
|
||||||
|
value: null
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
active: !!params.categories,
|
active: !!params.categories,
|
||||||
choices: mapNodeToChoice(
|
choices: mapNodeToChoice(
|
||||||
|
|
|
@ -5,14 +5,12 @@ import { fetchMoreProps, searchPageProps } from "@saleor/fixtures";
|
||||||
import { ProductListFilterOpts } from "@saleor/products/components/ProductListPage";
|
import { ProductListFilterOpts } from "@saleor/products/components/ProductListPage";
|
||||||
import { productTypes } from "@saleor/productTypes/fixtures";
|
import { productTypes } from "@saleor/productTypes/fixtures";
|
||||||
import { StockAvailability } from "@saleor/types/globalTypes";
|
import { StockAvailability } from "@saleor/types/globalTypes";
|
||||||
|
import { mapEdgesToItems, mapSlugNodeToChoice } from "@saleor/utils/maps";
|
||||||
|
|
||||||
export const productListFilterOpts: ProductListFilterOpts = {
|
export const productListFilterOpts: ProductListFilterOpts = {
|
||||||
attributes: attributes.map(attr => ({
|
attributes: attributes.map(attr => ({
|
||||||
|
id: attr.id,
|
||||||
active: false,
|
active: false,
|
||||||
choices: attr.choices.edges.map(val => ({
|
|
||||||
label: val.node.name,
|
|
||||||
value: val.node.slug
|
|
||||||
})),
|
|
||||||
name: attr.name,
|
name: attr.name,
|
||||||
slug: attr.slug,
|
slug: attr.slug,
|
||||||
value: [
|
value: [
|
||||||
|
@ -20,6 +18,14 @@ export const productListFilterOpts: ProductListFilterOpts = {
|
||||||
attr.choices.edges.length > 2 && attr.choices.edges[2].node.slug
|
attr.choices.edges.length > 2 && attr.choices.edges[2].node.slug
|
||||||
]
|
]
|
||||||
})),
|
})),
|
||||||
|
attributeChoices: {
|
||||||
|
...fetchMoreProps,
|
||||||
|
...searchPageProps,
|
||||||
|
active: false,
|
||||||
|
value: null,
|
||||||
|
choices: mapSlugNodeToChoice(mapEdgesToItems(attributes[0].choices)),
|
||||||
|
displayValues: mapSlugNodeToChoice(mapEdgesToItems(attributes[0].choices))
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
...fetchMoreProps,
|
...fetchMoreProps,
|
||||||
...searchPageProps,
|
...searchPageProps,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -107,6 +107,7 @@ export interface FilterPageProps<TKeys extends string, TOpts extends {}>
|
||||||
export interface FilterProps<TKeys extends string> {
|
export interface FilterProps<TKeys extends string> {
|
||||||
currencySymbol?: string;
|
currencySymbol?: string;
|
||||||
onFilterChange: (filter: IFilter<TKeys>) => void;
|
onFilterChange: (filter: IFilter<TKeys>) => void;
|
||||||
|
onFilterAttributeFocus?: (id?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabPageProps {
|
export interface TabPageProps {
|
||||||
|
@ -133,6 +134,9 @@ export interface PartialMutationProviderOutput<
|
||||||
export interface Node {
|
export interface Node {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
export interface SlugNode {
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type Pagination = Partial<{
|
export type Pagination = Partial<{
|
||||||
after: string;
|
after: string;
|
||||||
|
|
|
@ -72,7 +72,8 @@ export function createAutocompleteField<T extends string>(
|
||||||
displayValues: MultiAutocompleteChoiceType[],
|
displayValues: MultiAutocompleteChoiceType[],
|
||||||
multiple: boolean,
|
multiple: boolean,
|
||||||
options: MultiAutocompleteChoiceType[],
|
options: MultiAutocompleteChoiceType[],
|
||||||
opts: FetchMoreProps & SearchPageProps
|
opts: FetchMoreProps & SearchPageProps,
|
||||||
|
id?: string
|
||||||
): IFilterElement<T> {
|
): IFilterElement<T> {
|
||||||
return {
|
return {
|
||||||
...opts,
|
...opts,
|
||||||
|
@ -83,7 +84,8 @@ export function createAutocompleteField<T extends string>(
|
||||||
name,
|
name,
|
||||||
options,
|
options,
|
||||||
type: FieldType.autocomplete,
|
type: FieldType.autocomplete,
|
||||||
value: defaultValue
|
value: defaultValue,
|
||||||
|
id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo"
|
||||||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
import { 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 } from "@saleor/types";
|
import { Node, SlugNode } from "@saleor/types";
|
||||||
import { MetadataInput } from "@saleor/types/globalTypes";
|
import { MetadataInput } from "@saleor/types/globalTypes";
|
||||||
|
|
||||||
interface EdgesType<T> {
|
interface EdgesType<T> {
|
||||||
|
@ -49,6 +49,19 @@ export function mapNodeToChoice(
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export function mapMetadataItemToInput(item: MetadataItem): MetadataInput {
|
export function mapMetadataItemToInput(item: MetadataItem): MetadataInput {
|
||||||
return {
|
return {
|
||||||
key: item.key,
|
key: item.key,
|
||||||
|
|
Loading…
Reference in a new issue