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:
Dawid Tarasiuk 2021-06-14 15:31:41 +02:00 committed by GitHub
parent 99aa6365be
commit c3e720a47e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 2145 additions and 941 deletions

View file

@ -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

View file

@ -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"]'
}, },

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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: {

View file

@ -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> {

View file

@ -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}

View file

@ -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}

View file

@ -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

View file

@ -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`

View file

@ -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;
}

View file

@ -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}

View file

@ -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(

View file

@ -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

View file

@ -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;

View file

@ -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
}; };
} }

View file

@ -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,