Skip to content

Commit

Permalink
feat(commerce): handle sort and pagination in url and parameter manag…
Browse files Browse the repository at this point in the history
…ers (#4018)

Add support for sort and pagination in the url and parameter managers.

I initially piggy-backed on the existing sort criterion
serialization/deserialization logic, but writing our own leads to a
simpler implementation.

To test, run `yarn link @coveo/headless` on
coveo/barca-sports#270.

🎩 
<img width="700" alt="image"
src="https://github.com/coveo/ui-kit/assets/8978908/2c6ae204-ca55-467c-8757-310966e7fcaf">

[CAPI-907]

[CAPI-907]:
https://coveord.atlassian.net/browse/CAPI-907?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
Spuffynism authored May 28, 2024
1 parent cfca30c commit 5fbb087
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {
toggleSelectNumericFacetValue,
} from '../../facets/range-facets/numeric-facet-set/numeric-facet-actions';
import {setContext, setUser, setView} from '../context/context-actions';
import {restoreProductListingParameters} from '../product-listing-parameters/product-listing-parameter-actions';
import {fetchProductListing} from '../product-listing/product-listing-actions';
import {fetchRecommendations} from '../recommendations/recommendations-actions';
import {restoreSearchParameters} from '../search-parameters/search-parameters-actions';
import {executeSearch} from '../search/search-actions';
import {
nextPage,
Expand Down Expand Up @@ -206,6 +208,40 @@ describe('pagination slice', () => {
).toEqual(pagination);
});

describe.each([
{
action: restoreSearchParameters,
actionName: 'restoreSearchParameters',
},
{
action: restoreProductListingParameters,
actionName: 'restoreProductListingParameters',
},
])('$actionName', ({action}) => {
it('restores principal pagination', () => {
const parameters = {
page: 2,
perPage: 11,
};

const finalState = paginationReducer(state, action(parameters));

expect(finalState.principal.page).toBe(parameters.page);
expect(finalState.principal.perPage).toBe(parameters.perPage);
});

it('does not restore principal pagination when parameters are not defined', () => {
const parameters = {
page: undefined,
perPage: undefined,
};

const finalState = paginationReducer(state, action(parameters));

expect(finalState.principal).toBe(state.principal);
});
});

describe.each([
{
actionName: '#deselectAllFacetValues',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import {
toggleSelectNumericFacetValue,
} from '../../facets/range-facets/numeric-facet-set/numeric-facet-actions';
import {setContext, setUser, setView} from '../context/context-actions';
import {Parameters} from '../parameters/parameters-actions';
import {restoreProductListingParameters} from '../product-listing-parameters/product-listing-parameter-actions';
import {fetchProductListing} from '../product-listing/product-listing-actions';
import {fetchRecommendations} from '../recommendations/recommendations-actions';
import {restoreSearchParameters} from '../search-parameters/search-parameters-actions';
import {executeSearch} from '../search/search-actions';
import {
nextPage,
Expand Down Expand Up @@ -93,6 +96,8 @@ export const paginationReducer = createReducer(

state.recommendations[slotId] = getCommercePaginationInitialSlice();
})
.addCase(restoreSearchParameters, handleRestoreParameters)
.addCase(restoreProductListingParameters, handleRestoreParameters)
.addCase(deselectAllFacetValues, handlePaginationReset)
.addCase(toggleSelectFacetValue, handlePaginationReset)
.addCase(toggleExcludeFacetValue, handlePaginationReset)
Expand All @@ -116,3 +121,16 @@ function getEffectiveSlice(
function handlePaginationReset(state: CommercePaginationState) {
state.principal.page = getCommercePaginationInitialSlice().page;
}

function handleRestoreParameters(
state: CommercePaginationState,
action: {payload: Parameters}
) {
if (action.payload.page) {
state.principal.page = action.payload.page;
}

if (action.payload.perPage) {
state.principal.perPage = action.payload.perPage;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {DateRangeRequest} from '../../facets/range-facets/date-facet-set/interfaces/request';
import {NumericRangeRequest} from '../../facets/range-facets/numeric-facet-set/interfaces/request';
import {SortCriterion} from '../sort/sort';

export interface Parameters {
/**
Expand All @@ -25,9 +26,7 @@ export interface Parameters {
/**
* The sort expression to order returned results by.
*/
// eslint-disable-next-line @cspell/spellchecker
// TODO CAPI-907: Handle sort and pagination
//sortCriteria?: SortCriterion;
sortCriteria?: SortCriterion;

/**
* The zero-based index of the active page.
Expand All @@ -37,7 +36,5 @@ export interface Parameters {
/**
* The number of results per page.
*/
// eslint-disable-next-line @cspell/spellchecker
// TODO CAPI-907: Handle sort and pagination
//perPage?: number;
perPage?: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import {CommerceEngine} from '../../../app/commerce-engine/commerce-engine';
import {stateKey} from '../../../app/state-key';
import {CommerceFacetSetSection} from '../../../state/state-sections';
import {findActiveValueAncestry} from '../../facets/category-facet-set/category-facet-utils';
import {getFacets} from '../../parameter-manager/parameter-manager-selectors';
import {
getFacets,
getSortCriteria,
} from '../../parameter-manager/parameter-manager-selectors';
import {FacetType} from '../facets/facet-set/interfaces/common';
import {
AnyFacetRequest,
Expand All @@ -11,7 +14,11 @@ import {
NumericFacetRequest,
RegularFacetRequest,
} from '../facets/facet-set/interfaces/request';
import {getCommercePaginationInitialSlice} from '../pagination/pagination-state';
import {
CommercePaginationState,
getCommercePaginationInitialSlice,
} from '../pagination/pagination-state';
import {getCommerceSortInitialState} from '../sort/sort-state';
import {Parameters as ManagedParameters} from './parameters-actions';

export function initialParametersSelector(
Expand All @@ -21,10 +28,12 @@ export function initialParametersSelector(
page:
state.commercePagination.principal.page ??
getCommercePaginationInitialSlice().page,
// eslint-disable-next-line @cspell/spellchecker
// TODO CAPI-907: Handle sort and pagination
// perPage: state.commercePagination.principal.perPage ?? getCommercePaginationInitialSlice().perPage,
// sortCriteria: state.commerceSort.appliedSort ?? getCommerceSortInitialState().appliedSort,
perPage:
state.commercePagination.principal.perPage ??
getCommercePaginationInitialSlice().perPage,
sortCriteria:
state.commerceSort.appliedSort ??
getCommerceSortInitialState().appliedSort,
cf: {},
nf: {},
df: {},
Expand All @@ -36,9 +45,21 @@ export function activeParametersSelector(
state: CommerceEngine[typeof stateKey]
): ManagedParameters {
return {
// eslint-disable-next-line @cspell/spellchecker
// TODO CAPI-907: Handle sort and pagination
//...getSortCriteria(state?.commerceSort, (s) => s.appliedSort, getCommerceSortInitialState().appliedSort),
...getPage(
state?.commercePagination,
(s) => s.principal.page,
getCommercePaginationInitialSlice().page
),
...getPerPage(
state?.commercePagination,
(s) => s.principal.perPage,
getCommercePaginationInitialSlice().perPage
),
...getSortCriteria(
state?.commerceSort,
(s) => s.appliedSort,
getCommerceSortInitialState().appliedSort
),
...getFacets(
state.commerceFacetSet,
facetIsOfType(state, 'regular'),
Expand Down Expand Up @@ -76,6 +97,34 @@ export function enrichedParametersSelector(
};
}

export function getPage(
section: CommercePaginationState | undefined,
pageSelector: (section: CommercePaginationState) => number,
initialState: number
) {
if (section === undefined) {
return {};
}

const page = pageSelector(section);
const shouldInclude = page !== initialState;
return shouldInclude ? {page} : {};
}

export function getPerPage(
section: CommercePaginationState | undefined,
perPageSelector: (section: CommercePaginationState) => number,
initialState: number
) {
if (section === undefined) {
return {};
}

const perPage = perPageSelector(section);
const shouldInclude = perPage !== initialState;
return shouldInclude ? {perPage} : {};
}

export function getSelectedValues(request: AnyFacetRequest) {
return (request as RegularFacetRequest).values
.filter((fv) => fv.state === 'selected')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {buildDateRange} from '../../../controllers/core/facets/range-facet/date-facet/date-range';
import {buildNumericRange} from '../../../controllers/core/facets/range-facet/numeric-facet/numeric-range';
import {CommerceSearchParameters} from '../search-parameters/search-parameters-actions';
import {buildFieldsSortCriterion, SortDirection} from '../sort/sort';
import {searchSerializer} from './parameters-serializer';

const someSpecialCharactersThatNeedsEncoding = [
Expand Down Expand Up @@ -91,14 +92,21 @@ describe('searchSerializer', () => {
}),
],
};
const parameters: Required<
Omit<CommerceSearchParameters, 'sortCriteria' | 'page' | 'perPage'>
> = {
const page = 4;
const perPage = 96;
const sortCriteria = buildFieldsSortCriterion([
{name: 'author', direction: SortDirection.Ascending},
{name: 'created', direction: SortDirection.Descending},
]);
const parameters: Required<CommerceSearchParameters> = {
q: 'some query',
f,
cf,
nf,
df,
page,
perPage,
sortCriteria,
};

const serialized = serialize(parameters);
Expand Down
Loading

0 comments on commit 5fbb087

Please sign in to comment.