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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13839,13 +13839,14 @@ export type InitialProductFilterProductTypesQueryHookResult = ReturnType<typeof
export type InitialProductFilterProductTypesLazyQueryHookResult = ReturnType<typeof useInitialProductFilterProductTypesLazyQuery>;
export type InitialProductFilterProductTypesQueryResult = Apollo.QueryResult<Types.InitialProductFilterProductTypesQuery, Types.InitialProductFilterProductTypesQueryVariables>;
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(
before: $before
after: $after
first: $first
last: $last
filter: $filter
where: $where
sortBy: $sort
channel: $channel
) {
@ -13888,6 +13889,7 @@ ${ProductListAttributeFragmentDoc}`;
* last: // value for 'last'
* before: // value for 'before'
* filter: // value for 'filter'
* where: // value for 'where'
* channel: // value for 'channel'
* sort: // value for 'sort'
* hasChannel: // value for 'hasChannel'

View file

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

View file

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

View file

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

View file

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

View file

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