diff --git a/.changeset/happy-books-walk.md b/.changeset/happy-books-walk.md new file mode 100644 index 000000000..5d347768d --- /dev/null +++ b/.changeset/happy-books-walk.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Experimental filters: fetch product list based on selected filters diff --git a/src/components/ConditionalFilter/API/InitialStateResponse.ts b/src/components/ConditionalFilter/API/InitialStateResponse.ts index 7a5c053b9..7fbc7bbb6 100644 --- a/src/components/ConditionalFilter/API/InitialStateResponse.ts +++ b/src/components/ConditionalFilter/API/InitialStateResponse.ts @@ -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()) { diff --git a/src/components/ConditionalFilter/API/initialState/useInitialAPIState.tsx b/src/components/ConditionalFilter/API/initialState/useInitialAPIState.tsx index 38a4a8415..a69bdce39 100644 --- a/src/components/ConditionalFilter/API/initialState/useInitialAPIState.tsx +++ b/src/components/ConditionalFilter/API/initialState/useInitialAPIState.tsx @@ -26,7 +26,7 @@ import { InitialAPIResponse } from "./types"; export interface InitialAPIState { data: InitialStateResponse; loading: boolean; - fetchQueries: (params: FetchingParams) => void; + fetchQueries: (params: FetchingParams) => Promise; } export const useProductInitialAPIState = (): InitialAPIState => { @@ -129,7 +129,7 @@ export const useProductInitialAPIState = (): InitialAPIState => { initialState.isPublished, initialState.isVisibleInListing, initialState.hasCategory, - initialState.giftCard + initialState.giftCard, ), ); setLoading(false); diff --git a/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts b/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts index ceb047b71..88236c623 100644 --- a/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts +++ b/src/components/ConditionalFilter/FilterElement/ConditionSelected.ts @@ -2,7 +2,6 @@ import { getDefaultByControlName } from "../controlsType"; import { ConditionItem } from "./ConditionOptions"; import { ConditionValue } from "./ConditionValue"; - export class ConditionSelected { private constructor( public value: ConditionValue, diff --git a/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts b/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts index c87dfc6ed..c0d9b6ffb 100644 --- a/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts +++ b/src/components/ConditionalFilter/ValueProvider/useUrlValueProvider.ts @@ -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([]); 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 { diff --git a/src/components/ConditionalFilter/context/provider.tsx b/src/components/ConditionalFilter/context/provider.tsx index aa360145d..9b31f3408 100644 --- a/src/components/ConditionalFilter/context/provider.tsx +++ b/src/components/ConditionalFilter/context/provider.tsx @@ -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 ( void; type Element = FilterContainer[number]; -export const useContainerState = (initialValue: FilterContainer) => { - const [value, setValue] = useState(initialValue); +export const useContainerState = (valueProvider: FilterValueProvider) => { + const [value, setValue] = useState([]); useEffect(() => { - if (value.length === 0 && initialValue.length > 0) { - setValue(initialValue); + if (!valueProvider.loading) { + setValue(valueProvider.value); } - }, [initialValue]); + }, [valueProvider.loading]); const isFilterElement = ( elIndex: number, diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 2e591c7d8..0881ad74d 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -13839,13 +13839,14 @@ export type InitialProductFilterProductTypesQueryHookResult = ReturnType; export type InitialProductFilterProductTypesQueryResult = Apollo.QueryResult; 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' diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index 39f089f00..568c0aa88 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -10498,6 +10498,7 @@ export type ProductListQueryVariables = Exact<{ last?: InputMaybe; before?: InputMaybe; filter?: InputMaybe; + where?: InputMaybe; channel?: InputMaybe; sort?: InputMaybe; hasChannel: Scalars['Boolean']; diff --git a/src/products/queries.ts b/src/products/queries.ts index 3b5d4d5e8..be4016b2c 100644 --- a/src/products/queries.ts +++ b/src/products/queries.ts @@ -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 ) { diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index 9e83425a2..380fd63d5 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -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 = ({ params }) => { const navigate = useNavigator(); const notify = useNotifier(); const { queue } = useBackgroundTask(); + const { valueProvider } = useConditionalFilterContext(); + const productListingPageFiltersFlag = useFlag("product_filters"); const { updateListSettings, settings } = useListSettings( ListViews.PRODUCT_LIST, @@ -93,51 +97,59 @@ export const ProductList: React.FC = ({ 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(); const searchAttributeValues = useAttributeValueSearch({ @@ -271,18 +283,26 @@ export const ProductList: React.FC = ({ 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 >( () => ({ ...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 = ({ params }) => { ...queryVariables, hasChannel: !!selectedChannel, }, + skip: valueProvider.loading, }); const products = mapEdgesToItems(data?.products); @@ -391,7 +412,7 @@ export const ProductList: React.FC = ({ 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 = ({ params }) => { variables: { input: { ...data, - filter, + ...filterVariables, ids: selectedRowIds, }, }, diff --git a/src/products/views/ProductList/filters.test.ts b/src/products/views/ProductList/filters.test.ts index 7323cf06b..0c918fa28 100644 --- a/src/products/views/ProductList/filters.test.ts +++ b/src/products/views/ProductList/filters.test.ts @@ -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); }); diff --git a/src/products/views/ProductList/filters.ts b/src/products/views/ProductList/filters.ts index 02ce0218f..02c6944bf 100644 --- a/src/products/views/ProductList/filters.ts +++ b/src/products/views/ProductList/filters.ts @@ -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) }; +};