Experimental filters: fetch products based on selected filters (#3955)
This commit is contained in:
parent
fa0e142829
commit
33b4199cec
13 changed files with 104 additions and 30 deletions
5
.changeset/happy-books-walk.md
Normal file
5
.changeset/happy-books-walk.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-dashboard": patch
|
||||
---
|
||||
|
||||
Experimental filters: fetch product list based on selected filters
|
|
@ -37,8 +37,8 @@ export class InitialStateResponse implements InitialState {
|
|||
public isPublished: ItemOption[] = [],
|
||||
public isVisibleInListing: ItemOption[] = [],
|
||||
public hasCategory: ItemOption[] = [],
|
||||
public giftCard: ItemOption[] = []
|
||||
) { }
|
||||
public giftCard: ItemOption[] = [],
|
||||
) {}
|
||||
|
||||
public attributeByName(name: string) {
|
||||
return this.attribute[name];
|
||||
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -2,7 +2,6 @@ import { getDefaultByControlName } from "../controlsType";
|
|||
import { ConditionItem } from "./ConditionOptions";
|
||||
import { ConditionValue } from "./ConditionValue";
|
||||
|
||||
|
||||
export class ConditionSelected {
|
||||
private constructor(
|
||||
public value: ConditionValue,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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) };
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue