Skip to content

Commit

Permalink
feat(commerce): extract core pagination for plp and search (#3475)
Browse files Browse the repository at this point in the history
* extract core pagination for plp and search

* update pagination on search fulfilled

* split tests across describes

* bring shared constant up

* use shared reducer loaders
  • Loading branch information
Spuffynism authored Dec 12, 2023
1 parent fc563a8 commit 023fa90
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 137 deletions.
9 changes: 5 additions & 4 deletions packages/headless/src/commerce.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ export type {
export {buildProductListing} from './controllers/commerce/product-listing/headless-product-listing';

export type {
ProductListingPagination,
ProductListingPaginationState,
ProductListingPaginationControllerState,
} from './controllers/commerce/product-listing/pagination/headless-product-listing-pagination';
Pagination,
PaginationState,
PaginationControllerState,
} from './controllers/commerce/pagination/core/headless-core-commerce-pagination';
export {buildProductListingPagination} from './controllers/commerce/product-listing/pagination/headless-product-listing-pagination';
export {buildSearchPagination} from './controllers/commerce/search/pagination/headless-search-pagination';

export type {
InteractiveResult,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
selectPage,
nextPage,
previousPage,
} from '../../../../features/commerce/pagination/pagination-actions';
import {paginationReducer as commercePagination} from '../../../../features/commerce/pagination/pagination-slice';
import {fetchProductListing} from '../../../../features/commerce/product-listing/product-listing-actions';
import {buildMockCommerceEngine, MockCommerceEngine} from '../../../../test';
import {
buildCorePagination,
Pagination,
} from './headless-core-commerce-pagination';

describe('core pagination', () => {
let engine: MockCommerceEngine;
let pagination: Pagination;
const fetchResultsActionCreator = fetchProductListing;

function initPagination() {
engine = buildMockCommerceEngine();

pagination = buildCorePagination(engine, {fetchResultsActionCreator});
}

beforeEach(() => {
initPagination();
});

it('adds correct reducers to engine', () => {
expect(engine.addReducers).toBeCalledWith({
commercePagination,
});
});

it('exposes #subscribe method', () => {
expect(pagination.subscribe).toBeTruthy();
});

describe('#selectPage', () => {
beforeEach(() => {
pagination.selectPage(0);
});

it('dispatches #selectPage', () => {
expect(engine.actions).toContainEqual(selectPage(0));
});

it('dispatches #fetchResultsActionCreator', () => {
const action = engine.findAsyncAction(fetchResultsActionCreator.pending);
expect(action).toBeTruthy();
});
});

describe('#nextPage', () => {
beforeEach(() => {
pagination.nextPage();
});

it('dispatches #nextPage', () => {
expect(engine.actions).toContainEqual(nextPage());
});

it('dispatches #fetchResultsActionCreator', () => {
const action = engine.findAsyncAction(fetchResultsActionCreator.pending);
expect(action).toBeTruthy();
});
});

describe('#previousPage', () => {
beforeEach(() => {
pagination.previousPage();
});

it('dispatches #previousPage', () => {
expect(engine.actions).toContainEqual(previousPage());
});

it('dispatches #fetchResultsActionCreator', () => {
const action = engine.findAsyncAction(fetchResultsActionCreator.pending);
expect(action).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {AsyncThunkAction} from '@reduxjs/toolkit';
import {CommerceEngine} from '../../../../app/commerce-engine/commerce-engine';
import {
nextPage,
selectPage,
previousPage,
} from '../../../../features/commerce/pagination/pagination-actions';
import {paginationReducer as commercePagination} from '../../../../features/commerce/pagination/pagination-slice';
import {loadReducerError} from '../../../../utils/errors';
import {
Controller,
buildController,
} from '../../../controller/headless-controller';

/**
* The `Pagination` controller is responsible for navigating between pages of results in a commerce interface.
*/
export interface Pagination extends Controller {
/**
* Navigates to a specific page.
*
* @param page - The page to navigate to.
*/
selectPage(page: number): void;

/**
* Navigates to the next page.
*/
nextPage(): void;

/**
* Navigates to the previous page.
*/
previousPage(): void;

/**
* A scoped and simplified part of the headless state that is relevant to the `Pagination` controller.
*/
state: PaginationState;
}

export interface PaginationState {
page: number;
perPage: number;
totalCount: number;
totalPages: number;
}

export type PaginationControllerState = Pagination['state'];

export interface CorePaginationProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetchResultsActionCreator: () => AsyncThunkAction<unknown, void, any>;
}

/**
* @internal
* Creates a `Pagination` controller instance.
*
* @param engine - The headless commerce engine.
* @returns A `Pagination` controller instance.
* */
export function buildCorePagination(
engine: CommerceEngine,
props: CorePaginationProps
): Pagination {
if (!loadPaginationReducers(engine)) {
throw loadReducerError;
}
const controller = buildController(engine);
const {dispatch} = engine;

const getState = () => {
return engine.state.commercePagination;
};

return {
...controller,

get state() {
return getState();
},

selectPage(page: number) {
dispatch(selectPage(page));
dispatch(props.fetchResultsActionCreator());
},

nextPage() {
dispatch(nextPage());
dispatch(props.fetchResultsActionCreator());
},

previousPage() {
dispatch(previousPage());
dispatch(props.fetchResultsActionCreator());
},
};
}

function loadPaginationReducers(
engine: CommerceEngine
): engine is CommerceEngine {
engine.addReducers({
commercePagination,
});
return true;
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import {
selectPage,
nextPage,
previousPage,
} from '../../../../features/commerce/pagination/pagination-actions';
import {paginationReducer as commercePagination} from '../../../../features/commerce/pagination/pagination-slice';
import {fetchProductListing} from '../../../../features/commerce/product-listing/product-listing-actions';
import {productListingV2Reducer as productListing} from '../../../../features/commerce/product-listing/product-listing-slice';
import {buildMockCommerceEngine, MockCommerceEngine} from '../../../../test';
import {
buildProductListingPagination,
ProductListingPagination,
} from './headless-product-listing-pagination';
import {Pagination} from '../../pagination/core/headless-core-commerce-pagination';
import {buildProductListingPagination} from './headless-product-listing-pagination';

describe('ProductListingPagination', () => {
describe('product listing pagination', () => {
let engine: MockCommerceEngine;
let productListingPagination: ProductListingPagination;
let productListingPagination: Pagination;

function initProductListingPagination() {
engine = buildMockCommerceEngine();
Expand All @@ -29,33 +21,27 @@ describe('ProductListingPagination', () => {
it('adds correct reducers to engine', () => {
expect(engine.addReducers).toBeCalledWith({
productListing,
commercePagination,
});
});

it('exposes #subscribe method', () => {
expect(productListingPagination.subscribe).toBeTruthy();
});

it('#selectPage dispatches #selectPage & #fetchProductListing', () => {
it('#selectPage dispatches #fetchProductListing', () => {
productListingPagination.selectPage(0);
expect(engine.actions.find((a) => a.type === selectPage.type)).toBeTruthy();
const action = engine.findAsyncAction(fetchProductListing.pending);
expect(action).toBeTruthy();
});

it('#nextPage dispatches #nextPage & #fetchProductListing', () => {
it('#nextPage dispatches #fetchProductListing', () => {
productListingPagination.nextPage();
expect(engine.actions.find((a) => a.type === nextPage.type)).toBeTruthy();
const action = engine.findAsyncAction(fetchProductListing.pending);
expect(action).toBeTruthy();
});

it('#previousPage dispatches #previousPage & #fetchProductListing', () => {
it('#previousPage dispatches #fetchProductListing', () => {
productListingPagination.previousPage();
expect(
engine.actions.find((a) => a.type === previousPage.type)
).toBeTruthy();
const action = engine.findAsyncAction(fetchProductListing.pending);
expect(action).toBeTruthy();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,104 +1,26 @@
import {CommerceEngine} from '../../../../app/commerce-engine/commerce-engine';
import {
nextPage,
selectPage,
previousPage,
} from '../../../../features/commerce/pagination/pagination-actions';
import {paginationReducer as commercePagination} from '../../../../features/commerce/pagination/pagination-slice';
import {fetchProductListing} from '../../../../features/commerce/product-listing/product-listing-actions';
import {productListingV2Reducer as productListing} from '../../../../features/commerce/product-listing/product-listing-slice';
import {loadReducerError} from '../../../../utils/errors';
import {
Controller,
buildController,
} from '../../../controller/headless-controller';
buildCorePagination,
Pagination,
} from '../../pagination/core/headless-core-commerce-pagination';
import {loadProductListingReducer} from '../utils/load-product-listing-reducers';

/**
* The `ProductListingPagination` controller is responsible for navigating between pages of results in a commerce product listing interface.
*/
export interface ProductListingPagination extends Controller {
/**
* Navigates to a specific page.
*
* @param page The page to navigate to.
*/
selectPage(page: number): void;

/**
* Navigates to the next page.
*/
nextPage(): void;

/**
* Navigates to the previous page.
*/
previousPage(): void;

/**
* A scoped and simplified part of the headless state that is relevant to the `ProductListingPagination` controller.
*/
state: ProductListingPaginationState;
}

export interface ProductListingPaginationState {
page: number;
perPage: number;
totalCount: number;
totalPages: number;
}

export type ProductListingPaginationControllerState =
ProductListingPagination['state'];

/**
* Creates a `ProductListingPagination` controller instance.
* Creates a `Pagination` controller instance.
*
* @param engine - The headless commerce engine.
* @returns A `ProductListingPagination` controller instance.
* @returns A `Pagination` controller instance.
* */
export function buildProductListingPagination(
engine: CommerceEngine
): ProductListingPagination {
if (!loadProductListingPaginationReducers(engine)) {
): Pagination {
if (!loadProductListingReducer(engine)) {
throw loadReducerError;
}
const controller = buildController(engine);
const {dispatch} = engine;

const getState = () => {
return engine.state.commercePagination!;
};

return {
...controller,

get state() {
return getState();
},

selectPage(page: number) {
dispatch(selectPage(page));
dispatch(fetchProductListing());
},

nextPage() {
dispatch(nextPage());
dispatch(fetchProductListing());
},

previousPage() {
dispatch(previousPage());
dispatch(fetchProductListing());
},
};

function loadProductListingPaginationReducers(
engine: CommerceEngine
): engine is CommerceEngine {
engine.addReducers({
productListing,
commercePagination,
});
return true;
}
return buildCorePagination(engine, {
fetchResultsActionCreator: fetchProductListing,
});
}
Loading

0 comments on commit 023fa90

Please sign in to comment.