Experimental filters: fetch products based on selected filters (#3955)

This commit is contained in:
Krzysztof Żuraw 2023-07-19 15:22:43 +02:00 committed by GitHub
parent fa0e142829
commit 33b4199cec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 104 additions and 30 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---
Experimental filters: fetch product list based on selected filters

View file

@ -37,7 +37,7 @@ export class InitialStateResponse implements InitialState {
public isPublished: ItemOption[] = [], public isPublished: ItemOption[] = [],
public isVisibleInListing: ItemOption[] = [], public isVisibleInListing: ItemOption[] = [],
public hasCategory: ItemOption[] = [], public hasCategory: ItemOption[] = [],
public giftCard: ItemOption[] = [] public giftCard: ItemOption[] = [],
) {} ) {}
public attributeByName(name: string) { public attributeByName(name: string) {
@ -56,10 +56,13 @@ export class InitialStateResponse implements InitialState {
} }
if (token.isAttribute()) { if (token.isAttribute()) {
const attr = this.attribute[token.name] const attr = this.attribute[token.name];
return attr.inputType === "BOOLEAN" return attr.inputType === "BOOLEAN"
? createBoleanOption(token.value === "true", AttributeInputTypeEnum.BOOLEAN) ? createBoleanOption(
: token.value token.value === "true",
AttributeInputTypeEnum.BOOLEAN,
)
: token.value;
} }
if (!token.isLoadable()) { if (!token.isLoadable()) {

View file

@ -26,7 +26,7 @@ import { InitialAPIResponse } from "./types";
export interface InitialAPIState { export interface InitialAPIState {
data: InitialStateResponse; data: InitialStateResponse;
loading: boolean; loading: boolean;
fetchQueries: (params: FetchingParams) => void; fetchQueries: (params: FetchingParams) => Promise<void>;
} }
export const useProductInitialAPIState = (): InitialAPIState => { export const useProductInitialAPIState = (): InitialAPIState => {
@ -129,7 +129,7 @@ export const useProductInitialAPIState = (): InitialAPIState => {
initialState.isPublished, initialState.isPublished,
initialState.isVisibleInListing, initialState.isVisibleInListing,
initialState.hasCategory, initialState.hasCategory,
initialState.giftCard initialState.giftCard,
), ),
); );
setLoading(false); setLoading(false);

View file

@ -2,7 +2,6 @@ import { getDefaultByControlName } from "../controlsType";
import { ConditionItem } from "./ConditionOptions"; import { ConditionItem } from "./ConditionOptions";
import { ConditionValue } from "./ConditionValue"; import { ConditionValue } from "./ConditionValue";
export class ConditionSelected { export class ConditionSelected {
private constructor( private constructor(
public value: ConditionValue, public value: ConditionValue,

View file

@ -1,5 +1,5 @@
import { stringify } from "qs"; import { stringify } from "qs";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import useRouter from "use-react-router"; import useRouter from "use-react-router";
import { InitialAPIState } from "../API"; import { InitialAPIState } from "../API";
@ -29,6 +29,7 @@ export const useUrlValueProvider = (
const router = useRouter(); const router = useRouter();
const params = new URLSearchParams(router.location.search); const params = new URLSearchParams(router.location.search);
const { data, loading, fetchQueries } = initialState; const { data, loading, fetchQueries } = initialState;
const [value, setValue] = useState<FilterContainer>([]);
params.delete("asc"); params.delete("asc");
params.delete("sort"); params.delete("sort");
@ -39,13 +40,16 @@ export const useUrlValueProvider = (
fetchQueries(fetchingParams); fetchQueries(fetchingParams);
}, []); }, []);
const value = loading ? [] : tokenizedUrl.asFilterValuesFromResponse(data); useEffect(() => {
setValue(tokenizedUrl.asFilterValuesFromResponse(data));
}, [data]);
const persist = (filterValue: FilterContainer) => { const persist = (filterValue: FilterContainer) => {
router.history.replace({ router.history.replace({
pathname: router.location.pathname, pathname: router.location.pathname,
search: stringify(prepareStructure(filterValue)), search: stringify(prepareStructure(filterValue)),
}); });
setValue(filterValue);
}; };
return { return {

View file

@ -12,7 +12,7 @@ export const ConditionalProductFilterProvider: FC = ({ children }) => {
const initialState = useProductInitialAPIState(); const initialState = useProductInitialAPIState();
const valueProvider = useUrlValueProvider(initialState); const valueProvider = useUrlValueProvider(initialState);
const leftOperandsProvider = useFilterLeftOperandsProvider(); const leftOperandsProvider = useFilterLeftOperandsProvider();
const containerState = useContainerState(valueProvider.value); const containerState = useContainerState(valueProvider);
return ( return (
<ConditionalFilterContext.Provider <ConditionalFilterContext.Provider

View file

@ -1,18 +1,19 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FilterContainer, FilterElement } from "./FilterElement"; import { FilterContainer, FilterElement } from "./FilterElement";
import { FilterValueProvider } from "./FilterValueProvider";
type StateCallback = (el: FilterElement) => void; type StateCallback = (el: FilterElement) => void;
type Element = FilterContainer[number]; type Element = FilterContainer[number];
export const useContainerState = (initialValue: FilterContainer) => { export const useContainerState = (valueProvider: FilterValueProvider) => {
const [value, setValue] = useState(initialValue); const [value, setValue] = useState<FilterContainer>([]);
useEffect(() => { useEffect(() => {
if (value.length === 0 && initialValue.length > 0) { if (!valueProvider.loading) {
setValue(initialValue); setValue(valueProvider.value);
} }
}, [initialValue]); }, [valueProvider.loading]);
const isFilterElement = ( const isFilterElement = (
elIndex: number, elIndex: number,

View file

@ -13839,13 +13839,14 @@ export type InitialProductFilterProductTypesQueryHookResult = ReturnType<typeof
export type InitialProductFilterProductTypesLazyQueryHookResult = ReturnType<typeof useInitialProductFilterProductTypesLazyQuery>; export type InitialProductFilterProductTypesLazyQueryHookResult = ReturnType<typeof useInitialProductFilterProductTypesLazyQuery>;
export type InitialProductFilterProductTypesQueryResult = Apollo.QueryResult<Types.InitialProductFilterProductTypesQuery, Types.InitialProductFilterProductTypesQueryVariables>; export type InitialProductFilterProductTypesQueryResult = Apollo.QueryResult<Types.InitialProductFilterProductTypesQuery, Types.InitialProductFilterProductTypesQueryVariables>;
export const ProductListDocument = gql` export const ProductListDocument = gql`
query ProductList($first: Int, $after: String, $last: Int, $before: String, $filter: ProductFilterInput, $channel: String, $sort: ProductOrder, $hasChannel: Boolean!) { query ProductList($first: Int, $after: String, $last: Int, $before: String, $filter: ProductFilterInput, $where: ProductWhereInput, $channel: String, $sort: ProductOrder, $hasChannel: Boolean!) {
products( products(
before: $before before: $before
after: $after after: $after
first: $first first: $first
last: $last last: $last
filter: $filter filter: $filter
where: $where
sortBy: $sort sortBy: $sort
channel: $channel channel: $channel
) { ) {
@ -13888,6 +13889,7 @@ ${ProductListAttributeFragmentDoc}`;
* last: // value for 'last' * last: // value for 'last'
* before: // value for 'before' * before: // value for 'before'
* filter: // value for 'filter' * filter: // value for 'filter'
* where: // value for 'where'
* channel: // value for 'channel' * channel: // value for 'channel'
* sort: // value for 'sort' * sort: // value for 'sort'
* hasChannel: // value for 'hasChannel' * hasChannel: // value for 'hasChannel'

View file

@ -10498,6 +10498,7 @@ export type ProductListQueryVariables = Exact<{
last?: InputMaybe<Scalars['Int']>; last?: InputMaybe<Scalars['Int']>;
before?: InputMaybe<Scalars['String']>; before?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<ProductFilterInput>; filter?: InputMaybe<ProductFilterInput>;
where?: InputMaybe<ProductWhereInput>;
channel?: InputMaybe<Scalars['String']>; channel?: InputMaybe<Scalars['String']>;
sort?: InputMaybe<ProductOrder>; sort?: InputMaybe<ProductOrder>;
hasChannel: Scalars['Boolean']; hasChannel: Scalars['Boolean'];

View file

@ -61,6 +61,7 @@ export const productListQuery = gql`
$last: Int $last: Int
$before: String $before: String
$filter: ProductFilterInput $filter: ProductFilterInput
$where: ProductWhereInput
$channel: String $channel: String
$sort: ProductOrder $sort: ProductOrder
$hasChannel: Boolean! $hasChannel: Boolean!
@ -71,6 +72,7 @@ export const productListQuery = gql`
first: $first first: $first
last: $last last: $last
filter: $filter filter: $filter
where: $where
sortBy: $sort sortBy: $sort
channel: $channel channel: $channel
) { ) {

View file

@ -2,6 +2,7 @@
import { filterable } from "@dashboard/attributes/utils/data"; import { filterable } from "@dashboard/attributes/utils/data";
import ActionDialog from "@dashboard/components/ActionDialog"; import ActionDialog from "@dashboard/components/ActionDialog";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
import { useConditionalFilterContext } from "@dashboard/components/ConditionalFilter/context";
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog"; import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog"; import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog";
import { useShopLimitsQuery } from "@dashboard/components/Shop/queries"; import { useShopLimitsQuery } from "@dashboard/components/Shop/queries";
@ -12,6 +13,7 @@ import {
ProductListColumns, ProductListColumns,
} from "@dashboard/config"; } from "@dashboard/config";
import { Task } from "@dashboard/containers/BackgroundTasks/types"; import { Task } from "@dashboard/containers/BackgroundTasks/types";
import { useFlag } from "@dashboard/featureFlags";
import { import {
ProductListQueryVariables, ProductListQueryVariables,
useAvailableColumnAttributesLazyQuery, useAvailableColumnAttributesLazyQuery,
@ -84,6 +86,8 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const { queue } = useBackgroundTask(); const { queue } = useBackgroundTask();
const { valueProvider } = useConditionalFilterContext();
const productListingPageFiltersFlag = useFlag("product_filters");
const { updateListSettings, settings } = useListSettings<ProductListColumns>( const { updateListSettings, settings } = useListSettings<ProductListColumns>(
ListViews.PRODUCT_LIST, ListViews.PRODUCT_LIST,
@ -93,51 +97,59 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
const intl = useIntl(); const intl = useIntl();
const { data: initialFilterAttributes } = const { data: initialFilterAttributes } =
useInitialProductFilterAttributesQuery(); useInitialProductFilterAttributesQuery({
skip: productListingPageFiltersFlag.enabled,
});
const { data: initialFilterCategories } = const { data: initialFilterCategories } =
useInitialProductFilterCategoriesQuery({ useInitialProductFilterCategoriesQuery({
variables: { variables: {
categories: params.categories, categories: params.categories,
}, },
skip: !params.categories?.length, skip: !params.categories?.length || productListingPageFiltersFlag.enabled,
}); });
const { data: initialFilterCollections } = const { data: initialFilterCollections } =
useInitialProductFilterCollectionsQuery({ useInitialProductFilterCollectionsQuery({
variables: { variables: {
collections: params.collections, collections: params.collections,
}, },
skip: !params.collections?.length, skip:
!params.collections?.length || productListingPageFiltersFlag.enabled,
}); });
const { data: initialFilterProductTypes } = const { data: initialFilterProductTypes } =
useInitialProductFilterProductTypesQuery({ useInitialProductFilterProductTypesQuery({
variables: { variables: {
productTypes: params.productTypes, productTypes: params.productTypes,
}, },
skip: !params.productTypes?.length, skip:
!params.productTypes?.length || productListingPageFiltersFlag.enabled,
}); });
const searchCategories = useCategorySearch({ const searchCategories = useCategorySearch({
variables: { variables: {
...DEFAULT_INITIAL_SEARCH_DATA, ...DEFAULT_INITIAL_SEARCH_DATA,
first: 5, first: 5,
}, },
skip: productListingPageFiltersFlag.enabled,
}); });
const searchCollections = useCollectionSearch({ const searchCollections = useCollectionSearch({
variables: { variables: {
...DEFAULT_INITIAL_SEARCH_DATA, ...DEFAULT_INITIAL_SEARCH_DATA,
first: 5, first: 5,
}, },
skip: productListingPageFiltersFlag.enabled,
}); });
const searchProductTypes = useProductTypeSearch({ const searchProductTypes = useProductTypeSearch({
variables: { variables: {
...DEFAULT_INITIAL_SEARCH_DATA, ...DEFAULT_INITIAL_SEARCH_DATA,
first: 5, first: 5,
}, },
skip: productListingPageFiltersFlag.enabled,
}); });
const searchAttributes = useAttributeSearch({ const searchAttributes = useAttributeSearch({
variables: { variables: {
...DEFAULT_INITIAL_SEARCH_DATA, ...DEFAULT_INITIAL_SEARCH_DATA,
first: 10, first: 10,
}, },
skip: productListingPageFiltersFlag.enabled,
}); });
const [focusedAttribute, setFocusedAttribute] = useState<string>(); const [focusedAttribute, setFocusedAttribute] = useState<string>();
const searchAttributeValues = useAttributeValueSearch({ const searchAttributeValues = useAttributeValueSearch({
@ -271,18 +283,26 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
const channelOpts = availableChannels const channelOpts = availableChannels
? mapNodeToChoice(availableChannels, channel => channel.slug) ? mapNodeToChoice(availableChannels, channel => channel.slug)
: null; : null;
const filter = getFilterVariables(params, !!selectedChannel);
const filterVariables = getFilterVariables({
isProductListingPageFiltersFlagEnabled:
productListingPageFiltersFlag.enabled,
filterContainer: valueProvider.value,
queryParams: params,
isChannelSelected: !!selectedChannel,
});
const sort = getSortQueryVariables(params, !!selectedChannel); const sort = getSortQueryVariables(params, !!selectedChannel);
const queryVariables = React.useMemo< const queryVariables = React.useMemo<
Omit<ProductListQueryVariables, "hasChannel" | "hasSelectedAttributes"> Omit<ProductListQueryVariables, "hasChannel" | "hasSelectedAttributes">
>( >(
() => ({ () => ({
...paginationState, ...paginationState,
filter, ...filterVariables,
sort, sort,
channel: selectedChannel?.slug, channel: selectedChannel?.slug,
}), }),
[params, settings.rowNumber], [params, settings.rowNumber, valueProvider.value],
); );
const filteredColumnIds = (settings.columns ?? []) const filteredColumnIds = (settings.columns ?? [])
@ -295,6 +315,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
...queryVariables, ...queryVariables,
hasChannel: !!selectedChannel, hasChannel: !!selectedChannel,
}, },
skip: valueProvider.loading,
}); });
const products = mapEdgesToItems(data?.products); const products = mapEdgesToItems(data?.products);
@ -391,7 +412,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
gridAttributesOpts={gridAttributesOpts} gridAttributesOpts={gridAttributesOpts}
settings={settings} settings={settings}
availableColumnsAttributesOpts={availableColumnsAttributesOpts} availableColumnsAttributesOpts={availableColumnsAttributesOpts}
disabled={loading} disabled={loading || valueProvider.loading}
limits={limitOpts.data?.shop.limits} limits={limitOpts.data?.shop.limits}
products={products} products={products}
onUpdateListSettings={(...props) => { onUpdateListSettings={(...props) => {
@ -472,7 +493,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
variables: { variables: {
input: { input: {
...data, ...data,
filter, ...filterVariables,
ids: selectedRowIds, ids: selectedRowIds,
}, },
}, },

View file

@ -13,7 +13,7 @@ import {
FilterParam, FilterParam,
getAttributeValuesFromParams, getAttributeValuesFromParams,
getFilterQueryParam, getFilterQueryParam,
getFilterVariables, getLegacyFilterVariables,
mapAttributeParamsToFilterOpts, mapAttributeParamsToFilterOpts,
parseFilterValue, parseFilterValue,
} from "./filters"; } from "./filters";
@ -22,7 +22,7 @@ import { productListFilterOpts } from "./fixtures";
describe("Filtering query params", () => { describe("Filtering query params", () => {
it("should be empty object if no params given", () => { it("should be empty object if no params given", () => {
const params: ProductListUrlFilters = {}; const params: ProductListUrlFilters = {};
const filterVariables = getFilterVariables(params, undefined); const filterVariables = getLegacyFilterVariables(params, undefined);
expect(getExistingKeys(filterVariables)).toHaveLength(0); expect(getExistingKeys(filterVariables)).toHaveLength(0);
}); });
@ -34,7 +34,7 @@ describe("Filtering query params", () => {
status: true.toString(), status: true.toString(),
stockStatus: StockAvailability.IN_STOCK, stockStatus: StockAvailability.IN_STOCK,
}; };
const filterVariables = getFilterVariables(params, true); const filterVariables = getLegacyFilterVariables(params, true);
expect(getExistingKeys(filterVariables)).toHaveLength(2); expect(getExistingKeys(filterVariables)).toHaveLength(2);
}); });

View file

@ -1,5 +1,8 @@
// @ts-strict-ignore // @ts-strict-ignore
import { FilterContainer } from "@dashboard/components/ConditionalFilter/FilterElement";
import { createProductQueryVariables } from "@dashboard/components/ConditionalFilter/queryVariables";
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";
import { FlagValue } from "@dashboard/featureFlags/FlagContent";
import { import {
AttributeFragment, AttributeFragment,
AttributeInputTypeEnum, AttributeInputTypeEnum,
@ -8,6 +11,7 @@ import {
InitialProductFilterCollectionsQuery, InitialProductFilterCollectionsQuery,
InitialProductFilterProductTypesQuery, InitialProductFilterProductTypesQuery,
ProductFilterInput, ProductFilterInput,
ProductWhereInput,
SearchAttributeValuesQuery, SearchAttributeValuesQuery,
SearchAttributeValuesQueryVariables, SearchAttributeValuesQueryVariables,
SearchCategoriesQuery, SearchCategoriesQuery,
@ -367,7 +371,8 @@ function getFilteredAttributeValue(
return attrValues; return attrValues;
} }
export function getFilterVariables( // TODO: Remove this function when productListingPageFiltersFlag is removed
export function getLegacyFilterVariables(
params: ProductListUrlFilters, params: ProductListUrlFilters,
isChannelSelected: boolean, isChannelSelected: boolean,
): ProductFilterInput { ): ProductFilterInput {
@ -473,3 +478,34 @@ export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
...ProductListUrlFiltersWithMultipleValues, ...ProductListUrlFiltersWithMultipleValues,
...ProductListUrlFiltersAsDictWithMultipleValues, ...ProductListUrlFiltersAsDictWithMultipleValues,
}); });
export const getWhereVariables = (
productListingPageFiltersFlag: FlagValue,
value: FilterContainer,
): ProductWhereInput => {
if (productListingPageFiltersFlag.enabled) {
const queryVars = createProductQueryVariables(value);
return queryVars;
}
return undefined;
};
export const getFilterVariables = ({
isProductListingPageFiltersFlagEnabled,
filterContainer,
queryParams,
isChannelSelected,
}: {
isProductListingPageFiltersFlagEnabled: boolean;
filterContainer: FilterContainer;
queryParams: ProductListUrlFilters;
isChannelSelected: boolean;
}) => {
if (isProductListingPageFiltersFlagEnabled) {
const queryVars = createProductQueryVariables(filterContainer);
return { where: queryVars, search: queryParams.query };
}
return { filter: getLegacyFilterVariables(queryParams, isChannelSelected) };
};