From 9d638ae95e4e3a4691eb203dda7889bed341b27d Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Thu, 19 Oct 2023 16:03:28 +1030 Subject: [PATCH 01/32] Cursor pagination works when changing page sizes and moving forwards, not backwards --- .../interfaces/graphqlPageInfo.interface.ts | 13 +++++ .../graphqlPaginatedResult.interface.ts | 15 +---- .../graphql/src/services/graphql.service.ts | 57 +++++++++++++++++-- 3 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 packages/graphql/src/interfaces/graphqlPageInfo.interface.ts diff --git a/packages/graphql/src/interfaces/graphqlPageInfo.interface.ts b/packages/graphql/src/interfaces/graphqlPageInfo.interface.ts new file mode 100644 index 000000000..34e44f956 --- /dev/null +++ b/packages/graphql/src/interfaces/graphqlPageInfo.interface.ts @@ -0,0 +1,13 @@ +export type PageInfo = { +/** Do we have a next page from current cursor position? */ + hasNextPage: boolean; + + /** Do we have a previous page from current cursor position? */ + hasPreviousPage: boolean; + + /** What is the last cursor? */ + endCursor: string; + + /** What is the first cursor? */ + startCursor: string; +}; \ No newline at end of file diff --git a/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts b/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts index d14ab9704..6705a8ea5 100644 --- a/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts +++ b/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts @@ -1,4 +1,5 @@ import type { Metrics } from '@slickgrid-universal/common'; +import { PageInfo } from './graphqlPageInfo.interface'; export interface GraphqlPaginatedResult { data: { @@ -18,19 +19,7 @@ export interface GraphqlPaginatedResult { } /** Page information of the current cursor, do we have a next page and what is the end cursor? */ - pageInfo?: { - /** Do we have a next page from current cursor position? */ - hasNextPage: boolean; - - /** Do we have a previous page from current cursor position? */ - hasPreviousPage: boolean; - - /** What is the last cursor? */ - endCursor: string; - - /** What is the first cursor? */ - startCursor: string; - }; + pageInfo?: PageInfo; } }; diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index fe5d00223..0ef44b76a 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -36,6 +36,7 @@ import { } from '../interfaces/index'; import QueryBuilder from './graphqlQueryBuilder'; +import { PageInfo } from '../interfaces/graphqlPageInfo.interface'; const DEFAULT_ITEMS_PER_PAGE = 25; const DEFAULT_PAGE_SIZE = 20; @@ -53,6 +54,11 @@ export class GraphqlService implements BackendService { first: DEFAULT_ITEMS_PER_PAGE, offset: 0 }; + defaultCursorPaginationOptions: GraphqlCursorPaginationOption = { + first: DEFAULT_ITEMS_PER_PAGE, + after: undefined + }; + pageInfo: PageInfo | undefined; /** Getter for the Column Definitions */ get columnDefinitions() { @@ -146,7 +152,17 @@ export class GraphqlService implements BackendService { first: ((this.options.paginationOptions && this.options.paginationOptions.first) ? this.options.paginationOptions.first : ((this.pagination && this.pagination.pageSize) ? this.pagination.pageSize : null)) || this.defaultPaginationOptions.first }; - if (!this.options.isWithCursor) { + if (this.options.isWithCursor) { + if (this.options.paginationOptions) { + const { before, after } = this.options.paginationOptions as GraphqlCursorPaginationOption; + if (before) { + datasetFilters.before = before; + } else if (after) { + datasetFilters.after = after; + } + } + } + else { const paginationOptions = this.options?.paginationOptions; datasetFilters.offset = paginationOptions?.hasOwnProperty('offset') ? +(paginationOptions as any)['offset'] : 0; } @@ -497,16 +513,33 @@ export class GraphqlService implements BackendService { * @param pageSize */ updatePagination(newPage: number, pageSize: number) { + const previousPage = this._currentPagination?.pageNumber; + this._currentPagination = { pageNumber: newPage, pageSize }; - let paginationOptions; + let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption; if (this.options && this.options.isWithCursor) { - paginationOptions = { - first: pageSize - }; + const graphQlCursorPaginationOptions = { + first: pageSize, + } as GraphqlCursorPaginationOption; + + // can only navigate backwards or forwards if an existing PageInfo is set + if (this.pageInfo && previousPage && newPage > 1) { + if (newPage === previousPage) { + // stay on same "page", get data from the current cursor + graphQlCursorPaginationOptions.after = this.pageInfo.startCursor; + } else if(newPage > previousPage) { + // navigating forwards + graphQlCursorPaginationOptions.after = this.pageInfo.endCursor; + } else if(newPage < previousPage) { + // navigating backwards + graphQlCursorPaginationOptions.before = this.pageInfo.startCursor; + } + } + paginationOptions = graphQlCursorPaginationOptions; } else { paginationOptions = { first: pageSize, @@ -517,6 +550,20 @@ export class GraphqlService implements BackendService { this.updateOptions({ paginationOptions }); } + /** + * Updates the PageInfo when using cursor based pagination + * @param pageInfo The PageInfo object returned from the server + * @param totalCount The total count of items (often returned from the server in the same request) + */ + updatePageInfo(pageInfo: PageInfo, totalCount: number) { + console.assert(this.options?.isWithCursor, 'Updating PageInfo is only relevenat when using cursor pagination'); + this.pageInfo = pageInfo; + + if (this.pagination) { + this.pagination.totalItems = totalCount; + } + } + /** * loop through all columns to inspect sorters & update backend service sortingOptions * @param columnFilters From fff039d99ee0e392f136876451c3e4fe13d92922 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Thu, 19 Oct 2023 16:24:19 +1030 Subject: [PATCH 02/32] Backwards navigation working --- .../graphql/src/services/graphql.service.ts | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index 0ef44b76a..4f90c595a 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -154,10 +154,12 @@ export class GraphqlService implements BackendService { if (this.options.isWithCursor) { if (this.options.paginationOptions) { - const { before, after } = this.options.paginationOptions as GraphqlCursorPaginationOption; + const { before, after, first, last } = this.options.paginationOptions as GraphqlCursorPaginationOption; if (before) { + datasetFilters.last = last; datasetFilters.before = before; } else if (after) { + datasetFilters.first = first; datasetFilters.after = after; } } @@ -520,26 +522,35 @@ export class GraphqlService implements BackendService { pageSize }; - let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption; + let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption = {}; if (this.options && this.options.isWithCursor) { - const graphQlCursorPaginationOptions = { - first: pageSize, - } as GraphqlCursorPaginationOption; - // can only navigate backwards or forwards if an existing PageInfo is set if (this.pageInfo && previousPage && newPage > 1) { if (newPage === previousPage) { - // stay on same "page", get data from the current cursor - graphQlCursorPaginationOptions.after = this.pageInfo.startCursor; + // stay on same "page", get data from the current cursor position (pageSize may have changed) + paginationOptions = { + first: pageSize, + after: this.pageInfo.startCursor + }; } else if(newPage > previousPage) { - // navigating forwards - graphQlCursorPaginationOptions.after = this.pageInfo.endCursor; + // navigating forwards - // // https://relay.dev/graphql/connections.htm#sec-Forward-pagination-arguments + paginationOptions = { + first: pageSize, + after: this.pageInfo.endCursor + }; } else if(newPage < previousPage) { - // navigating backwards - graphQlCursorPaginationOptions.before = this.pageInfo.startCursor; + // navigating backwards - // https://relay.dev/graphql/connections.htm#sec-Backward-pagination-arguments + paginationOptions = { + last: pageSize, + before: this.pageInfo.endCursor + }; } } - paginationOptions = graphQlCursorPaginationOptions; + else { + paginationOptions = { + first: pageSize, + }; + } } else { paginationOptions = { first: pageSize, From 0b337046f70a8024208dd058094ba0616489fcf7 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Thu, 19 Oct 2023 17:01:31 +1030 Subject: [PATCH 03/32] Navigate to first page and last page using cursor pagination --- .../graphql/src/services/graphql.service.ts | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index 4f90c595a..32b88285a 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -58,7 +58,7 @@ export class GraphqlService implements BackendService { first: DEFAULT_ITEMS_PER_PAGE, after: undefined }; - pageInfo: PageInfo | undefined; + pageInfo: PageInfo & { totalCount: number} | undefined; /** Getter for the Column Definitions */ get columnDefinitions() { @@ -524,8 +524,25 @@ export class GraphqlService implements BackendService { let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption = {}; if (this.options && this.options.isWithCursor) { + // https://dev.to/jackmarchant/offset-and-cursor-pagination-explained-b89 + // Cursor based pagination does not allow navigation to the middle of the page. + // As such we treat any "page number" greater than the current page, to be a forward navigation. + // Likewise any page number less than the current page a backwards navigation. + // can only navigate backwards or forwards if an existing PageInfo is set - if (this.pageInfo && previousPage && newPage > 1) { + if (!this.pagination || !this.pageInfo || newPage === 1) { + // get the first page + paginationOptions = { + first: pageSize, + }; + } + else if (newPage === Math.ceil(this.pageInfo.totalCount / pageSize)) { + // get the last page + paginationOptions = { + last: pageSize, + }; + } + else if (this.pageInfo && previousPage) { if (newPage === previousPage) { // stay on same "page", get data from the current cursor position (pageSize may have changed) paginationOptions = { @@ -568,11 +585,10 @@ export class GraphqlService implements BackendService { */ updatePageInfo(pageInfo: PageInfo, totalCount: number) { console.assert(this.options?.isWithCursor, 'Updating PageInfo is only relevenat when using cursor pagination'); - this.pageInfo = pageInfo; - - if (this.pagination) { - this.pagination.totalItems = totalCount; - } + this.pageInfo = { + ...pageInfo, + totalCount + }; } /** From 4b46567c51f33cd372b72350a819dd3f9cac24fd Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 20 Oct 2023 15:31:56 +1030 Subject: [PATCH 04/32] updatePaginationOverloading... might refactor as optional argument at the end --- .../interfaces/backendService.interface.ts | 7 +- packages/common/src/interfaces/index.ts | 1 + .../src/interfaces/pageInfo.interface.ts | 13 ++ .../paginationCursorChangedArgs.interface.ts | 13 ++ .../common/src/services/pagination.service.ts | 26 ++- .../graphql/src/services/graphql.service.ts | 188 +++++++++++------- .../odata/src/services/grid-odata.service.ts | 15 +- 7 files changed, 181 insertions(+), 82 deletions(-) create mode 100644 packages/common/src/interfaces/pageInfo.interface.ts create mode 100644 packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts diff --git a/packages/common/src/interfaces/backendService.interface.ts b/packages/common/src/interfaces/backendService.interface.ts index 22333aecc..4ad5eacfe 100644 --- a/packages/common/src/interfaces/backendService.interface.ts +++ b/packages/common/src/interfaces/backendService.interface.ts @@ -8,6 +8,7 @@ import type { MultiColumnSort, Pagination, PaginationChangedArgs, + PaginationCursorChangedArgs, SingleColumnSort, } from './index'; import { SlickGrid } from './slickGrid.interface'; @@ -50,8 +51,8 @@ export interface BackendService { /** Update the Filters options with a set of new options */ updateFilters?: (columnFilters: ColumnFilters | CurrentFilter[], isUpdatedByPresetOrDynamically: boolean) => void; - /** Update the Pagination component with it's new page number and size */ - updatePagination?: (newPage: number, pageSize: number) => void; + /** Update the Pagination component with it's new page number and size. If using cursor based pagination also supply a PageInfo object */ + updatePagination?: (newPage: number, pageSize: number) => void | ((cursorArgs: PaginationCursorChangedArgs) => void); /** Update the Sorters options with a set of new options */ updateSorters?: (sortColumns?: Array, presetSorters?: CurrentSorter[]) => void; @@ -67,7 +68,7 @@ export interface BackendService { processOnFilterChanged: (event: Event | KeyboardEvent | undefined, args: FilterChangedArgs) => string; /** Execute when the pagination changed */ - processOnPaginationChanged: (event: Event | undefined, args: PaginationChangedArgs) => string; + processOnPaginationChanged: (event: Event | undefined, args: PaginationChangedArgs | PaginationCursorChangedArgs) => string; /** Execute when any of the sorters changed */ processOnSortChanged: (event: Event | undefined, args: SingleColumnSort | MultiColumnSort) => string; diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 987074f9c..85ff65e3f 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -120,6 +120,7 @@ export * from './onValidationErrorResult.interface'; export * from './operatorDetail.interface'; export * from './pagination.interface'; export * from './paginationChangedArgs.interface'; +export * from './paginationCursorChangedArgs.interface'; export * from './pagingInfo.interface'; export * from './resizeByContentOption.interface'; export * from './resizer.interface'; diff --git a/packages/common/src/interfaces/pageInfo.interface.ts b/packages/common/src/interfaces/pageInfo.interface.ts new file mode 100644 index 000000000..8023ec2e3 --- /dev/null +++ b/packages/common/src/interfaces/pageInfo.interface.ts @@ -0,0 +1,13 @@ +export interface PageInfo { + /** Do we have a next page from current cursor position? */ + hasNextPage: boolean; + + /** Do we have a previous page from current cursor position? */ + hasPreviousPage: boolean; + + /** What is the last cursor? */ + endCursor: string; + + /** What is the first cursor? */ + startCursor: string; +} diff --git a/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts b/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts new file mode 100644 index 000000000..b197edd76 --- /dev/null +++ b/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts @@ -0,0 +1,13 @@ +export interface PaginationCursorChangedArgs { + /** Start our page After cursor X */ + after?: string; + + /** Start our page Before cursor X */ + before?: string; + + /** Get first X number of objects */ + first?: number; + + /** Get last X number of objects */ + last?: number; +} diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 4b6f4b419..25888c964 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -5,6 +5,7 @@ import type { BackendServiceApi, CurrentPagination, Pagination, + PaginationCursorChangedArgs, ServicePagination, SlickDataView, SlickGrid, @@ -13,6 +14,7 @@ import type { import type { BackendUtilityService } from './backendUtility.service'; import type { SharedService } from './shared.service'; import type { Observable, RxJsFacade } from './rxjsFacade'; +import { PageInfo } from '../interfaces/pageInfo.interface'; // using external non-typed js libraries declare const Slick: SlickNamespace; @@ -86,6 +88,10 @@ export class PaginationService { } } + get cursorBased(): boolean { + return !!this._pageInfo; + } + addRxJsResource(rxjs: RxJsFacade) { this.rxjs = rxjs; } @@ -195,12 +201,19 @@ export class PaginationService { goToNextPage(event?: any, triggerChangeEvent = true): Promise { if (this._pageNumber < this._pageCount) { this._pageNumber++; - return triggerChangeEvent ? this.processOnPageChanged(this._pageNumber, event) : Promise.resolve(this.getFullPagination()); + if (triggerChangeEvent && this._pageInfo) { + return this.cursorBased + ? this.processOnPageChanged(this._pageNumber, event, { first: this._itemsPerPage, after: this._pageInfo.endCursor }) + : this.processOnPageChanged(this._pageNumber, event); + } else { + return Promise.resolve(this.getFullPagination()); + } } return Promise.resolve(false); } goToPageNumber(pageNumber: number, event?: any, triggerChangeEvent = true): Promise { + console.assert(!this.cursorBased, 'Cursor based navigation cannot navigate to arbitrary page'); const previousPageNumber = this._pageNumber; if (pageNumber < 1) { @@ -321,7 +334,7 @@ export class PaginationService { } } - processOnPageChanged(pageNumber: number, event?: Event | undefined): Promise { + processOnPageChanged(pageNumber: number, event?: Event | undefined, cursorArgs?: PaginationCursorChangedArgs): Promise { if (this.pubSubService.publish('onBeforePaginationChange', this.getFullPagination()) === false) { this.resetToPreviousPagination(); return Promise.resolve(this.getFullPagination()); @@ -347,7 +360,9 @@ export class PaginationService { } if (this._backendServiceApi?.process) { - const query = this._backendServiceApi.service.processOnPaginationChanged(event, { newPage: pageNumber, pageSize: itemsPerPage }); + const query = this.cursorBased && cursorArgs + ? this._backendServiceApi.service.processOnPaginationChanged(event, cursorArgs) + : this._backendServiceApi.service.processOnPaginationChanged(event, { newPage: pageNumber, pageSize: itemsPerPage }); // the processes can be Promises const process = this._backendServiceApi.process(query); @@ -437,6 +452,11 @@ export class PaginationService { } } + private _pageInfo?: PageInfo; + updatePageInfo(pageInfo: PageInfo) { + this._pageInfo = pageInfo; + } + updateTotalItems(totalItems: number, triggerChangedEvent = false) { this._totalItems = totalItems; if (this._paginationOptions) { diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index 32b88285a..bf5b23e60 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -13,6 +13,7 @@ import type { OperatorString, Pagination, PaginationChangedArgs, + PaginationCursorChangedArgs, SharedService, SingleColumnSort, SlickGrid, @@ -36,7 +37,6 @@ import { } from '../interfaces/index'; import QueryBuilder from './graphqlQueryBuilder'; -import { PageInfo } from '../interfaces/graphqlPageInfo.interface'; const DEFAULT_ITEMS_PER_PAGE = 25; const DEFAULT_PAGE_SIZE = 20; @@ -58,7 +58,6 @@ export class GraphqlService implements BackendService { first: DEFAULT_ITEMS_PER_PAGE, after: undefined }; - pageInfo: PageInfo & { totalCount: number} | undefined; /** Getter for the Column Definitions */ get columnDefinitions() { @@ -352,12 +351,17 @@ export class GraphqlService implements BackendService { * } * } */ - processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs): string { - const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); - this.updatePagination(args.newPage, pageSize); + processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs | PaginationCursorChangedArgs): string { + if ('pageSize' in args && 'newPage' in args) { + const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); + this.updatePagination(args.newPage, pageSize); + } else { + this.updatePagination(args); + } // build the GraphQL query which we will use in the WebAPI callback return this.buildQuery(); + } /* @@ -514,81 +518,123 @@ export class GraphqlService implements BackendService { * @param newPage * @param pageSize */ - updatePagination(newPage: number, pageSize: number) { - const previousPage = this._currentPagination?.pageNumber; - - this._currentPagination = { - pageNumber: newPage, - pageSize - }; - + updatePagination(newPage: number, pageSize: number): void; + updatePagination(cursorArgs: PaginationCursorChangedArgs): void; + updatePagination(newPageOrCursorArgs: number | PaginationCursorChangedArgs, pageSize?: number) { let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption = {}; - if (this.options && this.options.isWithCursor) { - // https://dev.to/jackmarchant/offset-and-cursor-pagination-explained-b89 - // Cursor based pagination does not allow navigation to the middle of the page. - // As such we treat any "page number" greater than the current page, to be a forward navigation. - // Likewise any page number less than the current page a backwards navigation. - - // can only navigate backwards or forwards if an existing PageInfo is set - if (!this.pagination || !this.pageInfo || newPage === 1) { - // get the first page - paginationOptions = { - first: pageSize, - }; - } - else if (newPage === Math.ceil(this.pageInfo.totalCount / pageSize)) { - // get the last page - paginationOptions = { - last: pageSize, - }; - } - else if (this.pageInfo && previousPage) { - if (newPage === previousPage) { - // stay on same "page", get data from the current cursor position (pageSize may have changed) - paginationOptions = { - first: pageSize, - after: this.pageInfo.startCursor - }; - } else if(newPage > previousPage) { - // navigating forwards - // // https://relay.dev/graphql/connections.htm#sec-Forward-pagination-arguments - paginationOptions = { - first: pageSize, - after: this.pageInfo.endCursor - }; - } else if(newPage < previousPage) { - // navigating backwards - // https://relay.dev/graphql/connections.htm#sec-Backward-pagination-arguments - paginationOptions = { - last: pageSize, - before: this.pageInfo.endCursor - }; - } - } - else { - paginationOptions = { - first: pageSize, - }; + + if (this.options?.isWithCursor) { + // use cursor based pagination + const cursorArgs = newPageOrCursorArgs; + if (cursorArgs instanceof Object) { + // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments + paginationOptions = newPageOrCursorArgs as PaginationCursorChangedArgs; + } else { + // should not happen unless there is an error in initial configuration + paginationOptions = this.defaultCursorPaginationOptions; } } else { + // use offset based pagination + const newPage = newPageOrCursorArgs as number; + this._currentPagination = { + pageNumber: newPage, + pageSize: pageSize! + }; paginationOptions = { first: pageSize, - offset: (newPage > 1) ? ((newPage - 1) * pageSize) : 0 // recalculate offset but make sure the result is always over 0 + offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 }; } - this.updateOptions({ paginationOptions }); - } + // if (typeof newPageOrCursorArgs === 'number' || !this.options?.isWithCursor) { + // const newPage = newPageOrCursorArgs as number; + // this._currentPagination = { + // pageNumber: newPage, + // pageSize: pageSize! + // }; + // paginationOptions = { + // first: pageSize, + // offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 + // }; + // } else { + // // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments + // paginationOptions = newPageOrCursorArgs as PaginationCursorChangedArgs; + // } + + // else { + // const newPage = newPageOrCursorArgs as number; + // this._currentPagination = { + // pageNumber: newPage, + // pageSize: pageSize! + // }; + // paginationOptions = { + // first: pageSize, + // offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 + // }; + // } + + + // if (this.options && this.options.isWithCursor) { + // https://dev.to/jackmarchant/offset-and-cursor-pagination-explained-b89 + // Cursor based pagination does not allow navigation to the middle of the page. + // As such we treat any "page number" greater than the current page, to be a forward navigation. + // Likewise any page number less than the current page a backwards navigation. + // if (cursorArgs) { + // paginationOptions = cursorArgs; + // } else { + // paginationOptions = { + // first: 0 + // }; + // } + + // can only navigate backwards or forwards if an existing PageInfo is set + // if (!this.pagination || !this.pageInfo || newPage === 1) { + // // get the first page + // paginationOptions = { + // first: pageSize, + // }; + // } + // else if (newPage === Math.ceil(this.pageInfo.totalCount / pageSize)) { + // // get the last page + // paginationOptions = { + // last: pageSize, + // }; + // } + // else if (this.pageInfo && previousPage) { + // if (newPage === previousPage) { + // // stay on same "page", get data from the current cursor position (pageSize may have changed) + // paginationOptions = { + // first: pageSize, + // after: this.pageInfo.startCursor + // }; + // } else if(newPage > previousPage) { + // // navigating forwards - // // https://relay.dev/graphql/connections.htm#sec-Forward-pagination-arguments + // paginationOptions = { + // first: pageSize, + // after: this.pageInfo.endCursor + // }; + // } else if(newPage < previousPage) { + // // navigating backwards - // https://relay.dev/graphql/connections.htm#sec-Backward-pagination-arguments + // paginationOptions = { + // last: pageSize, + // before: this.pageInfo.endCursor + // }; + // } + // } + // else { + // paginationOptions = { + // first: pageSize, + // }; + // } + + // } else { + // paginationOptions = { + // first: pageSize, + // offset: (newPage > 1) ? ((newPage - 1) * pageSize) : 0 // recalculate offset but make sure the result is always over 0 + // }; + // } - /** - * Updates the PageInfo when using cursor based pagination - * @param pageInfo The PageInfo object returned from the server - * @param totalCount The total count of items (often returned from the server in the same request) - */ - updatePageInfo(pageInfo: PageInfo, totalCount: number) { - console.assert(this.options?.isWithCursor, 'Updating PageInfo is only relevenat when using cursor pagination'); - this.pageInfo = { - ...pageInfo, - totalCount - }; + this.updateOptions({ paginationOptions }); } /** diff --git a/packages/odata/src/services/grid-odata.service.ts b/packages/odata/src/services/grid-odata.service.ts index 8c99ab84c..4cc0cbf15 100644 --- a/packages/odata/src/services/grid-odata.service.ts +++ b/packages/odata/src/services/grid-odata.service.ts @@ -19,6 +19,7 @@ import type { SharedService, SingleColumnSort, SlickGrid, + PaginationCursorChangedArgs, } from '@slickgrid-universal/common'; import { CaseType, @@ -267,12 +268,16 @@ export class GridOdataService implements BackendService { /* * PAGINATION */ - processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs) { - const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); - this.updatePagination(args.newPage, pageSize); + processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs | PaginationCursorChangedArgs) { + if ('pageSize' in args && 'newPage' in args) { + const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); + this.updatePagination(args.newPage, pageSize); - // build the OData query which we will use in the WebAPI callback - return this._odataService.buildQuery(); + // build the OData query which we will use in the WebAPI callback + return this._odataService.buildQuery(); + } + + return ''; // cursor based pagination not supported in this service } /* From e6c9fc70e4d3e730c97108d575d1a16d6dbd15b6 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 20 Oct 2023 16:31:36 +1030 Subject: [PATCH 05/32] graphql forward pagination --- .../interfaces/backendService.interface.ts | 2 +- .../paginationCursorChangedArgs.interface.ts | 4 +- .../common/src/services/pagination.service.ts | 4 +- .../graphql/src/services/graphql.service.ts | 261 ++++++++++-------- 4 files changed, 156 insertions(+), 115 deletions(-) diff --git a/packages/common/src/interfaces/backendService.interface.ts b/packages/common/src/interfaces/backendService.interface.ts index 4ad5eacfe..2c417d84f 100644 --- a/packages/common/src/interfaces/backendService.interface.ts +++ b/packages/common/src/interfaces/backendService.interface.ts @@ -52,7 +52,7 @@ export interface BackendService { updateFilters?: (columnFilters: ColumnFilters | CurrentFilter[], isUpdatedByPresetOrDynamically: boolean) => void; /** Update the Pagination component with it's new page number and size. If using cursor based pagination also supply a PageInfo object */ - updatePagination?: (newPage: number, pageSize: number) => void | ((cursorArgs: PaginationCursorChangedArgs) => void); + updatePagination?: (newPage: number, pageSize: number, cursorArgs?: PaginationCursorChangedArgs) => void; /** Update the Sorters options with a set of new options */ updateSorters?: (sortColumns?: Array, presetSorters?: CurrentSorter[]) => void; diff --git a/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts b/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts index b197edd76..ad5742697 100644 --- a/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts +++ b/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts @@ -1,4 +1,6 @@ -export interface PaginationCursorChangedArgs { +import { PaginationChangedArgs } from './paginationChangedArgs.interface'; + +export interface PaginationCursorChangedArgs extends PaginationChangedArgs { /** Start our page After cursor X */ after?: string; diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 25888c964..734afc88f 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -203,7 +203,7 @@ export class PaginationService { this._pageNumber++; if (triggerChangeEvent && this._pageInfo) { return this.cursorBased - ? this.processOnPageChanged(this._pageNumber, event, { first: this._itemsPerPage, after: this._pageInfo.endCursor }) + ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage, after: this._pageInfo.endCursor }) : this.processOnPageChanged(this._pageNumber, event); } else { return Promise.resolve(this.getFullPagination()); @@ -335,6 +335,8 @@ export class PaginationService { } processOnPageChanged(pageNumber: number, event?: Event | undefined, cursorArgs?: PaginationCursorChangedArgs): Promise { + console.assert(!this.cursorBased || cursorArgs, 'Configured for cursor based pagination - cursorArgs expected'); + if (this.pubSubService.publish('onBeforePaginationChange', this.getFullPagination()) === false) { this.resetToPreviousPagination(); return Promise.resolve(this.getFullPagination()); diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index bf5b23e60..053995c6b 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -50,13 +50,12 @@ export class GraphqlService implements BackendService { protected _datasetIdPropName = 'id'; options: GraphqlServiceOption | undefined; pagination: Pagination | undefined; - defaultPaginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption = { + defaultPaginationOptions: GraphqlPaginationOption = { first: DEFAULT_ITEMS_PER_PAGE, offset: 0 }; defaultCursorPaginationOptions: GraphqlCursorPaginationOption = { first: DEFAULT_ITEMS_PER_PAGE, - after: undefined }; /** Getter for the Column Definitions */ @@ -352,16 +351,17 @@ export class GraphqlService implements BackendService { * } */ processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs | PaginationCursorChangedArgs): string { - if ('pageSize' in args && 'newPage' in args) { - const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); - this.updatePagination(args.newPage, pageSize); - } else { - this.updatePagination(args); - } + const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); + this.updatePagination(args.newPage, pageSize); + + // if first/last defined on args, then it is a cursor based pagination change + 'first' in args || 'last' in args + ? this.updatePagination(args.newPage, pageSize, args) + : this.updatePagination(args.newPage, pageSize); + // build the GraphQL query which we will use in the WebAPI callback return this.buildQuery(); - } /* @@ -514,129 +514,166 @@ export class GraphqlService implements BackendService { } /** - * Update the pagination component with it's new page number and size + * Update the pagination component with it's new page number and size. * @param newPage * @param pageSize + * @param cursorArgs Should be supplied when using cursor based pagination */ - updatePagination(newPage: number, pageSize: number): void; - updatePagination(cursorArgs: PaginationCursorChangedArgs): void; - updatePagination(newPageOrCursorArgs: number | PaginationCursorChangedArgs, pageSize?: number) { - let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption = {}; + updatePagination(newPage: number, pageSize: number, cursorArgs?: PaginationCursorChangedArgs) { + this._currentPagination = { + pageNumber: newPage, + pageSize + }; + let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption = {}; if (this.options?.isWithCursor) { // use cursor based pagination - const cursorArgs = newPageOrCursorArgs; - if (cursorArgs instanceof Object) { + if (cursorArgs && cursorArgs instanceof Object) { // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments - paginationOptions = newPageOrCursorArgs as PaginationCursorChangedArgs; + // remove pageSize and newPage from cursorArgs, otherwise they get put on the query input string + + // eslint-disable-next-line + const { pageSize, newPage, ...cursorPaginationOptions } = cursorArgs; + paginationOptions = cursorPaginationOptions; } else { - // should not happen unless there is an error in initial configuration - paginationOptions = this.defaultCursorPaginationOptions; + // can happen when initial configuration not set correctly (automatically corrects itself next updatePageInfo() call) + paginationOptions = this.getInitPaginationOptions(); } } else { // use offset based pagination - const newPage = newPageOrCursorArgs as number; - this._currentPagination = { - pageNumber: newPage, - pageSize: pageSize! - }; paginationOptions = { first: pageSize, offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 }; } - // if (typeof newPageOrCursorArgs === 'number' || !this.options?.isWithCursor) { - // const newPage = newPageOrCursorArgs as number; - // this._currentPagination = { - // pageNumber: newPage, - // pageSize: pageSize! - // }; - // paginationOptions = { - // first: pageSize, - // offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 - // }; - // } else { - // // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments - // paginationOptions = newPageOrCursorArgs as PaginationCursorChangedArgs; - // } - - // else { - // const newPage = newPageOrCursorArgs as number; - // this._currentPagination = { - // pageNumber: newPage, - // pageSize: pageSize! - // }; - // paginationOptions = { - // first: pageSize, - // offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 - // }; - // } - - - // if (this.options && this.options.isWithCursor) { - // https://dev.to/jackmarchant/offset-and-cursor-pagination-explained-b89 - // Cursor based pagination does not allow navigation to the middle of the page. - // As such we treat any "page number" greater than the current page, to be a forward navigation. - // Likewise any page number less than the current page a backwards navigation. - // if (cursorArgs) { - // paginationOptions = cursorArgs; - // } else { - // paginationOptions = { - // first: 0 - // }; - // } - - // can only navigate backwards or forwards if an existing PageInfo is set - // if (!this.pagination || !this.pageInfo || newPage === 1) { - // // get the first page - // paginationOptions = { - // first: pageSize, - // }; - // } - // else if (newPage === Math.ceil(this.pageInfo.totalCount / pageSize)) { - // // get the last page - // paginationOptions = { - // last: pageSize, - // }; - // } - // else if (this.pageInfo && previousPage) { - // if (newPage === previousPage) { - // // stay on same "page", get data from the current cursor position (pageSize may have changed) - // paginationOptions = { - // first: pageSize, - // after: this.pageInfo.startCursor - // }; - // } else if(newPage > previousPage) { - // // navigating forwards - // // https://relay.dev/graphql/connections.htm#sec-Forward-pagination-arguments - // paginationOptions = { - // first: pageSize, - // after: this.pageInfo.endCursor - // }; - // } else if(newPage < previousPage) { - // // navigating backwards - // https://relay.dev/graphql/connections.htm#sec-Backward-pagination-arguments - // paginationOptions = { - // last: pageSize, - // before: this.pageInfo.endCursor - // }; - // } - // } - // else { - // paginationOptions = { - // first: pageSize, - // }; - // } - - // } else { - // paginationOptions = { - // first: pageSize, - // offset: (newPage > 1) ? ((newPage - 1) * pageSize) : 0 // recalculate offset but make sure the result is always over 0 - // }; - // } - this.updateOptions({ paginationOptions }); } + /** + * Update the pagination component with it's new page number and size + * @param newPage + * @param pageSize + */ + // updatePagination(newPage: number, pageSize: number): void; + // updatePagination(cursorArgs: PaginationCursorChangedArgs): void; + // updatePagination(newPageOrCursorArgs: number | PaginationCursorChangedArgs, pageSize?: number) { + // let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption = {}; + + // if (this.options?.isWithCursor) { + // // use cursor based pagination + // const cursorArgs = newPageOrCursorArgs; + // if (cursorArgs instanceof Object) { + // // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments + // paginationOptions = newPageOrCursorArgs as PaginationCursorChangedArgs; + // } else { + // // should not happen unless there is an error in initial configuration + // paginationOptions = this.defaultCursorPaginationOptions; + // } + // } else { + // // use offset based pagination + // const newPage = newPageOrCursorArgs as number; + // this._currentPagination = { + // pageNumber: newPage, + // pageSize: pageSize! + // }; + // paginationOptions = { + // first: pageSize, + // offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 + // }; + // } + + // // if (typeof newPageOrCursorArgs === 'number' || !this.options?.isWithCursor) { + // // const newPage = newPageOrCursorArgs as number; + // // this._currentPagination = { + // // pageNumber: newPage, + // // pageSize: pageSize! + // // }; + // // paginationOptions = { + // // first: pageSize, + // // offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 + // // }; + // // } else { + // // // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments + // // paginationOptions = newPageOrCursorArgs as PaginationCursorChangedArgs; + // // } + + // // else { + // // const newPage = newPageOrCursorArgs as number; + // // this._currentPagination = { + // // pageNumber: newPage, + // // pageSize: pageSize! + // // }; + // // paginationOptions = { + // // first: pageSize, + // // offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 + // // }; + // // } + + + // // if (this.options && this.options.isWithCursor) { + // // https://dev.to/jackmarchant/offset-and-cursor-pagination-explained-b89 + // // Cursor based pagination does not allow navigation to the middle of the page. + // // As such we treat any "page number" greater than the current page, to be a forward navigation. + // // Likewise any page number less than the current page a backwards navigation. + // // if (cursorArgs) { + // // paginationOptions = cursorArgs; + // // } else { + // // paginationOptions = { + // // first: 0 + // // }; + // // } + + // // can only navigate backwards or forwards if an existing PageInfo is set + // // if (!this.pagination || !this.pageInfo || newPage === 1) { + // // // get the first page + // // paginationOptions = { + // // first: pageSize, + // // }; + // // } + // // else if (newPage === Math.ceil(this.pageInfo.totalCount / pageSize)) { + // // // get the last page + // // paginationOptions = { + // // last: pageSize, + // // }; + // // } + // // else if (this.pageInfo && previousPage) { + // // if (newPage === previousPage) { + // // // stay on same "page", get data from the current cursor position (pageSize may have changed) + // // paginationOptions = { + // // first: pageSize, + // // after: this.pageInfo.startCursor + // // }; + // // } else if(newPage > previousPage) { + // // // navigating forwards - // // https://relay.dev/graphql/connections.htm#sec-Forward-pagination-arguments + // // paginationOptions = { + // // first: pageSize, + // // after: this.pageInfo.endCursor + // // }; + // // } else if(newPage < previousPage) { + // // // navigating backwards - // https://relay.dev/graphql/connections.htm#sec-Backward-pagination-arguments + // // paginationOptions = { + // // last: pageSize, + // // before: this.pageInfo.endCursor + // // }; + // // } + // // } + // // else { + // // paginationOptions = { + // // first: pageSize, + // // }; + // // } + + // // } else { + // // paginationOptions = { + // // first: pageSize, + // // offset: (newPage > 1) ? ((newPage - 1) * pageSize) : 0 // recalculate offset but make sure the result is always over 0 + // // }; + // // } + + // this.updateOptions({ paginationOptions }); + // } + /** * loop through all columns to inspect sorters & update backend service sortingOptions * @param columnFilters From 01ceb2bbccd4a1d2d28c84c454d32c68ee03cd2d Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 20 Oct 2023 17:24:36 +1030 Subject: [PATCH 06/32] Fix page backwards. Note for pagination with numbers to work correctly, requires non-relay pagination. relay pagination is suited to infinite scrolling --- .../common/src/services/pagination.service.ts | 8 ++++- .../graphql/src/services/graphql.service.ts | 34 +++++++++---------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 734afc88f..54256b7b9 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -233,7 +233,13 @@ export class PaginationService { goToPreviousPage(event?: any, triggerChangeEvent = true): Promise { if (this._pageNumber > 1) { this._pageNumber--; - return triggerChangeEvent ? this.processOnPageChanged(this._pageNumber, event) : Promise.resolve(this.getFullPagination()); + if (triggerChangeEvent && this._pageInfo) { + return this.cursorBased + ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage, before: this._pageInfo.startCursor }) + : this.processOnPageChanged(this._pageNumber, event); + } else { + return Promise.resolve(this.getFullPagination()); + } } return Promise.resolve(false); } diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index 053995c6b..0e8cb78da 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -145,25 +145,14 @@ export class GraphqlService implements BackendService { // only add pagination if it's enabled in the grid options if (this._gridOptions.enablePagination !== false) { - datasetFilters = { - ...this.options.paginationOptions, - first: ((this.options.paginationOptions && this.options.paginationOptions.first) ? this.options.paginationOptions.first : ((this.pagination && this.pagination.pageSize) ? this.pagination.pageSize : null)) || this.defaultPaginationOptions.first - }; + datasetFilters = {}; - if (this.options.isWithCursor) { - if (this.options.paginationOptions) { - const { before, after, first, last } = this.options.paginationOptions as GraphqlCursorPaginationOption; - if (before) { - datasetFilters.last = last; - datasetFilters.before = before; - } else if (after) { - datasetFilters.first = first; - datasetFilters.after = after; - } - } + if (this.options.isWithCursor && this.options.paginationOptions) { + datasetFilters = { ...this.options.paginationOptions }; } else { const paginationOptions = this.options?.paginationOptions; + datasetFilters.first = ((this.options.paginationOptions && this.options.paginationOptions.first) ? this.options.paginationOptions.first : ((this.pagination && this.pagination.pageSize) ? this.pagination.pageSize : null)) || this.defaultPaginationOptions.first; datasetFilters.offset = paginationOptions?.hasOwnProperty('offset') ? +(paginationOptions as any)['offset'] : 0; } } @@ -352,7 +341,6 @@ export class GraphqlService implements BackendService { */ processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs | PaginationCursorChangedArgs): string { const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); - this.updatePagination(args.newPage, pageSize); // if first/last defined on args, then it is a cursor based pagination change 'first' in args || 'last' in args @@ -614,8 +602,18 @@ export class GraphqlService implements BackendService { // // if (this.options && this.options.isWithCursor) { // // https://dev.to/jackmarchant/offset-and-cursor-pagination-explained-b89 // // Cursor based pagination does not allow navigation to the middle of the page. - // // As such we treat any "page number" greater than the current page, to be a forward navigation. - // // Likewise any page number less than the current page a backwards navigation. + //  // Pagination by page numbers only makes sense in non-relay style pagination + // // Relay style pagination is better suited to infinite scrolling + // // relay pagination - infinte scrolling appending data + // // page1: {startCursor: A, endCursor: B } + // // page2: {startCursor: A, endCursor: C } + // // page3: {startCursor: A, endCursor: D } + + // // non-relay pagination - Getting page chunks + // // page1: {startCursor: A, endCursor: B } + // // page2: {startCursor: B, endCursor: C } + // // page3: {startCursor: C, endCursor: D } + // // if (cursorArgs) { // // paginationOptions = cursorArgs; // // } else { From 13044cebe4b47ccc341b0b7bed8be79c5b3fc065 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Tue, 24 Oct 2023 13:25:22 +1030 Subject: [PATCH 07/32] got first/last page. forward/backwards navigate working --- .../common/src/services/pagination.service.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 54256b7b9..cf53854c6 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -190,19 +190,31 @@ export class PaginationService { goToFirstPage(event?: any, triggerChangeEvent = true): Promise { this._pageNumber = 1; - return triggerChangeEvent ? this.processOnPageChanged(this._pageNumber, event) : Promise.resolve(this.getFullPagination()); + if (triggerChangeEvent) { + return this.cursorBased && this._pageInfo + ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage }) + : this.processOnPageChanged(this._pageNumber, event); + } else { + return Promise.resolve(this.getFullPagination()); + } } goToLastPage(event?: any, triggerChangeEvent = true): Promise { this._pageNumber = this._pageCount || 1; - return triggerChangeEvent ? this.processOnPageChanged(this._pageNumber || 1, event) : Promise.resolve(this.getFullPagination()); + if (triggerChangeEvent) { + return this.cursorBased && this._pageInfo + ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage }) + : this.processOnPageChanged(this._pageNumber, event); + } else { + return Promise.resolve(this.getFullPagination()); + } } goToNextPage(event?: any, triggerChangeEvent = true): Promise { if (this._pageNumber < this._pageCount) { this._pageNumber++; - if (triggerChangeEvent && this._pageInfo) { - return this.cursorBased + if (triggerChangeEvent) { + return this.cursorBased && this._pageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage, after: this._pageInfo.endCursor }) : this.processOnPageChanged(this._pageNumber, event); } else { @@ -235,7 +247,7 @@ export class PaginationService { this._pageNumber--; if (triggerChangeEvent && this._pageInfo) { return this.cursorBased - ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage, before: this._pageInfo.startCursor }) + ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage, before: this._pageInfo.startCursor }) : this.processOnPageChanged(this._pageNumber, event); } else { return Promise.resolve(this.getFullPagination()); From a024918a5ee6e8055d723bc7bb6c404b2e6f661a Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Tue, 24 Oct 2023 14:04:59 +1030 Subject: [PATCH 08/32] pagination component is a normal text element for current page when in cursor pagination --- .../common/src/services/pagination.service.ts | 2 +- .../src/slick-pagination.component.ts | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index cf53854c6..f0960b363 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -89,7 +89,7 @@ export class PaginationService { } get cursorBased(): boolean { - return !!this._pageInfo; + return !!this._backendServiceApi?.options.isWithCursor; } addRxJsResource(rxjs: RxJsFacade) { diff --git a/packages/pagination-component/src/slick-pagination.component.ts b/packages/pagination-component/src/slick-pagination.component.ts index c2ccc895a..d4668842b 100644 --- a/packages/pagination-component/src/slick-pagination.component.ts +++ b/packages/pagination-component/src/slick-pagination.component.ts @@ -185,8 +185,10 @@ export class SlickPaginationComponent { this._bindingHelper.addElementBinding(this.currentPagination, 'dataTo', 'span.item-to', 'textContent'); this._bindingHelper.addElementBinding(this.currentPagination, 'totalItems', 'span.total-items', 'textContent'); this._bindingHelper.addElementBinding(this.currentPagination, 'pageCount', 'span.page-count', 'textContent'); - this._bindingHelper.addElementBinding(this.currentPagination, 'pageNumber', 'input.page-number', 'value', 'change', this.changeToCurrentPage.bind(this)); this._bindingHelper.addElementBinding(this.currentPagination, 'pageSize', 'select.items-per-page', 'value'); + this.paginationService.cursorBased + ? this._bindingHelper.addElementBinding(this.currentPagination, 'pageNumber', 'span.page-number', 'textContent') + : this._bindingHelper.addElementBinding(this.currentPagination, 'pageNumber', 'input.page-number', 'value', 'change', this.changeToCurrentPage.bind(this)); // locale text changes this._bindingHelper.addElementBinding(this, 'textItems', 'span.text-items', 'textContent'); @@ -283,12 +285,25 @@ export class SlickPaginationComponent { const divElm = createDomElement('div', { className: 'slick-page-number' }); createDomElement('span', { className: 'text-page', textContent: 'Page' }, divElm); divElm.appendChild(document.createTextNode(' ')); - createDomElement('input', { - type: 'text', className: 'form-control page-number', - ariaLabel: 'Page Number', - value: '1', size: 1, - dataset: { test: 'page-number-input' }, - }, divElm); + if (this.paginationService.cursorBased) { + // cursor based navigation cannot jump to an arbitrary page. Display current page. + createDomElement('span', { + className: 'page-number', + ariaLabel: 'Page Number', + dataset: { test: 'page-number-label' }, + textContent: '1', + }, divElm); + } else { + // offset based navigation can jump to any page. Allow editing of current page number. + createDomElement('input', { + type: 'text', + className: 'form-control page-number', + ariaLabel: 'Page Number', + value: '1', size: 1, + dataset: { test: 'page-number-input' }, + }, divElm); + } + divElm.appendChild(document.createTextNode(' ')); createDomElement('span', { className: 'text-of', textContent: 'of' }, divElm); divElm.appendChild(document.createTextNode(' ')); From fbee9d00e97bead4df5569335c855b7ebbe3d8d8 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Tue, 24 Oct 2023 16:19:14 +1030 Subject: [PATCH 09/32] unit tests passed --- packages/common/src/services/pagination.service.ts | 4 ++-- packages/graphql/src/services/graphql.service.ts | 7 +++---- packages/odata/src/services/grid-odata.service.ts | 3 ++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index f0960b363..8859d562f 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -245,8 +245,8 @@ export class PaginationService { goToPreviousPage(event?: any, triggerChangeEvent = true): Promise { if (this._pageNumber > 1) { this._pageNumber--; - if (triggerChangeEvent && this._pageInfo) { - return this.cursorBased + if (triggerChangeEvent) { + return this.cursorBased && this._pageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage, before: this._pageInfo.startCursor }) : this.processOnPageChanged(this._pageNumber, event); } else { diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index 0e8cb78da..3b194eff1 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -516,16 +516,15 @@ export class GraphqlService implements BackendService { let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption = {}; if (this.options?.isWithCursor) { // use cursor based pagination + // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments, + // but still handle the case where it's not (can happen when initial configuration not pre-configured (automatically corrects itself next updatePageInfo() call)) if (cursorArgs && cursorArgs instanceof Object) { - // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments // remove pageSize and newPage from cursorArgs, otherwise they get put on the query input string - // eslint-disable-next-line const { pageSize, newPage, ...cursorPaginationOptions } = cursorArgs; paginationOptions = cursorPaginationOptions; } else { - // can happen when initial configuration not set correctly (automatically corrects itself next updatePageInfo() call) - paginationOptions = this.getInitPaginationOptions(); + paginationOptions = { first: pageSize }; } } else { // use offset based pagination diff --git a/packages/odata/src/services/grid-odata.service.ts b/packages/odata/src/services/grid-odata.service.ts index 4cc0cbf15..8f44d982d 100644 --- a/packages/odata/src/services/grid-odata.service.ts +++ b/packages/odata/src/services/grid-odata.service.ts @@ -269,7 +269,7 @@ export class GridOdataService implements BackendService { * PAGINATION */ processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs | PaginationCursorChangedArgs) { - if ('pageSize' in args && 'newPage' in args) { + if ('pageSize' in args || 'newPage' in args) { const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); this.updatePagination(args.newPage, pageSize); @@ -277,6 +277,7 @@ export class GridOdataService implements BackendService { return this._odataService.buildQuery(); } + console.assert(true, 'cursor based pagination not supported in this service'); return ''; // cursor based pagination not supported in this service } From 59678d2a283311b0d2ac47fe6a5a40c1b19645d9 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Thu, 26 Oct 2023 10:36:26 +1030 Subject: [PATCH 10/32] slickpagination integration tests combined for cursor and non-cursor based pagination --- .../src/__tests__/slick-pagination.spec.ts | 365 ++++++++++-------- 1 file changed, 196 insertions(+), 169 deletions(-) diff --git a/packages/pagination-component/src/__tests__/slick-pagination.spec.ts b/packages/pagination-component/src/__tests__/slick-pagination.spec.ts index 7c5dd0dc9..b27ef46da 100644 --- a/packages/pagination-component/src/__tests__/slick-pagination.spec.ts +++ b/packages/pagination-component/src/__tests__/slick-pagination.spec.ts @@ -17,7 +17,7 @@ const gridStub = { const mockGridOptions = { enableTranslate: false } as GridOption; -const mockFullPagination = { +let mockFullPagination = { pageCount: 19, pageNumber: 2, pageSize: 5, @@ -27,7 +27,7 @@ const mockFullPagination = { dataTo: 15, }; -const paginationServiceStub = { +const basicPaginationServiceStub = { dataFrom: 10, dataTo: 15, pageNumber: 2, @@ -36,6 +36,7 @@ const paginationServiceStub = { pageSize: 5, totalItems: 95, availablePageSizes: [5, 10, 15, 20], + cursorBased: false, pageInfoTotalItems: jest.fn(), getFullPagination: jest.fn(), goToFirstPage: jest.fn(), @@ -47,12 +48,21 @@ const paginationServiceStub = { dispose: jest.fn(), init: jest.fn(), } as unknown as PaginationService; -Object.defineProperty(paginationServiceStub, 'dataFrom', { get: jest.fn(() => mockFullPagination.dataFrom), set: jest.fn() }); -Object.defineProperty(paginationServiceStub, 'dataTo', { get: jest.fn(() => mockFullPagination.dataTo), set: jest.fn() }); -Object.defineProperty(paginationServiceStub, 'pageCount', { get: jest.fn(() => mockFullPagination.pageCount), set: jest.fn() }); -Object.defineProperty(paginationServiceStub, 'pageNumber', { get: jest.fn(() => mockFullPagination.pageNumber), set: jest.fn() }); -Object.defineProperty(paginationServiceStub, 'itemsPerPage', { get: jest.fn(() => mockFullPagination.pageSize), set: jest.fn() }); -Object.defineProperty(paginationServiceStub, 'totalItems', { get: jest.fn(() => mockFullPagination.totalItems), set: jest.fn() }); + +const paginationServiceStubWithCursor = { + ...basicPaginationServiceStub, + cursorBased: true, +} as unknown as PaginationService + +[basicPaginationServiceStub, paginationServiceStubWithCursor].forEach(stub => { + + Object.defineProperty(stub, 'dataFrom', { get: jest.fn(() => mockFullPagination.dataFrom), set: jest.fn() }); + Object.defineProperty(stub, 'dataTo', { get: jest.fn(() => mockFullPagination.dataTo), set: jest.fn() }); + Object.defineProperty(stub, 'pageCount', { get: jest.fn(() => mockFullPagination.pageCount), set: jest.fn() }); + Object.defineProperty(stub, 'pageNumber', { get: jest.fn(() => mockFullPagination.pageNumber), set: jest.fn() }); + Object.defineProperty(stub, 'itemsPerPage', { get: jest.fn(() => mockFullPagination.pageSize), set: jest.fn() }); + Object.defineProperty(stub, 'totalItems', { get: jest.fn(() => mockFullPagination.totalItems), set: jest.fn() }); +}); describe('Slick-Pagination Component', () => { let component: SlickPaginationComponent; @@ -61,167 +71,180 @@ describe('Slick-Pagination Component', () => { let sharedService: SharedService; let translateService: TranslateServiceStub; - beforeEach(() => { - jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); - jest.spyOn(paginationServiceStub, 'getFullPagination').mockReturnValue(mockFullPagination); - jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(mockGridOptions); - div = document.createElement('div'); - document.body.appendChild(div); - sharedService = new SharedService(); - eventPubSubService = new EventPubSubService(); - translateService = new TranslateServiceStub(); - - component = new SlickPaginationComponent(paginationServiceStub, eventPubSubService, sharedService, translateService); - component.renderPagination(div); - }); - - describe('Integration Tests', () => { - afterEach(() => { - // clear all the spyOn mocks to not influence next test - jest.clearAllMocks(); - component.dispose(); - }); - - it('should make sure Slick-Pagination is defined', () => { - const paginationElm = document.querySelector('div.pager.slickgrid_123456') as HTMLSelectElement; - - expect(component).toBeTruthy(); - expect(component.constructor).toBeDefined(); - expect(paginationElm).toBeTruthy(); - }); - - it('should create a the Slick-Pagination component in the DOM', () => { - const pageInfoFromTo = document.querySelector('.page-info-from-to') as HTMLSpanElement; - const pageInfoTotalItems = document.querySelector('.page-info-total-items') as HTMLSpanElement; - const itemsPerPage = document.querySelector('.items-per-page') as HTMLSelectElement; - - expect(translateService.getCurrentLanguage()).toBe('en'); - expect(pageInfoFromTo.querySelector('span.item-from')!.ariaLabel).toBe('Page Item From'); // JSDom doesn't support ariaLabel, but we can test attribute this way - expect(pageInfoFromTo.querySelector('span.item-to')!.ariaLabel).toBe('Page Item To'); - expect(pageInfoTotalItems.querySelector('span.total-items')!.ariaLabel).toBe('Total Items'); - expect(removeExtraSpaces(pageInfoFromTo.innerHTML)).toBe('10-15 of '); - expect(removeExtraSpaces(pageInfoTotalItems.innerHTML)).toBe('95 items '); - expect(itemsPerPage.selectedOptions[0].value).toBe('5'); - }); - - it('should call changeToFirstPage() from the View and expect the pagination service to be called with correct method', () => { - const spy = jest.spyOn(paginationServiceStub, 'goToFirstPage'); - - const button = document.querySelector('.icon-seek-first') as HTMLAnchorElement; - button.click(); - mockFullPagination.pageNumber = 1; - mockFullPagination.dataFrom = 1; - mockFullPagination.dataTo = 10; - jest.spyOn(paginationServiceStub, 'dataFrom', 'get').mockReturnValue(mockFullPagination.dataFrom); - jest.spyOn(paginationServiceStub, 'dataTo', 'get').mockReturnValue(mockFullPagination.dataTo); - - const input = document.querySelector('input.form-control') as HTMLInputElement; - const itemFrom = document.querySelector('.item-from') as HTMLInputElement; - const itemTo = document.querySelector('.item-to') as HTMLInputElement; - - expect(spy).toHaveBeenCalled(); - expect(input.value).toBe('1'); - expect(component.dataFrom).toBe(1); - expect(component.dataTo).toBe(10); - expect(component.itemsPerPage).toBe(5); - expect(itemFrom.textContent).toBe('1'); - expect(itemTo.textContent).toBe('10'); - }); - - it('should call changeToNextPage() from the View and expect the pagination service to be called with correct method', () => { - const spy = jest.spyOn(paginationServiceStub, 'goToNextPage'); - - const button = document.querySelector('.icon-seek-next') as HTMLAnchorElement; - button.click(); - - expect(spy).toHaveBeenCalled(); - }); - - it('should call changeToPreviousPage() from the View and expect the pagination service to be called with correct method', () => { - mockFullPagination.pageNumber = 2; - const spy = jest.spyOn(paginationServiceStub, 'goToPreviousPage'); - - const button = document.querySelector('.icon-seek-prev') as HTMLAnchorElement; - button.click(); - - expect(spy).toHaveBeenCalled(); - }); - - it('should call changeToLastPage() from the View and expect the pagination service to be called with correct method', () => { - const spy = jest.spyOn(paginationServiceStub, 'goToLastPage'); - - const button = document.querySelector('.icon-seek-end') as HTMLAnchorElement; - button.click(); - - expect(spy).toHaveBeenCalled(); - }); - - it('should change the page number and expect the pagination service to go to that page', () => { - const spy = jest.spyOn(paginationServiceStub, 'goToPageNumber'); - - const newPageNumber = 3; - const input = document.querySelector('input.form-control') as HTMLInputElement; - input.value = `${newPageNumber}`; - const mockEvent = new CustomEvent('change', { bubbles: true, detail: { target: { value: newPageNumber } } }); - input.dispatchEvent(mockEvent); - component.pageNumber = newPageNumber; - - expect(spy).toHaveBeenCalledWith(newPageNumber); - }); - - it('should change the changeItemPerPage select dropdown and expect the pagination service call a change', () => { - const spy = jest.spyOn(paginationServiceStub, 'changeItemPerPage'); - - const newItemsPerPage = 10; - const select = document.querySelector('select') as HTMLSelectElement; - select.value = `${newItemsPerPage}`; - const mockEvent = new CustomEvent('change', { bubbles: true, detail: { target: { value: newItemsPerPage } } }); - select.dispatchEvent(mockEvent); - - expect(spy).toHaveBeenCalledWith(newItemsPerPage); - }); - - it(`should trigger "onPaginationRefreshed" and expect page from/to being displayed when total items is over 0 and also expect first/prev buttons to be disabled when on page 1`, () => { - mockFullPagination.pageNumber = 1; - mockFullPagination.totalItems = 100; - component.pageNumber = 1; - eventPubSubService.publish('onPaginationRefreshed', mockFullPagination); - const pageFromToElm = document.querySelector('span.page-info-from-to') as HTMLSpanElement; - - expect(component.firstButtonClasses).toBe('page-item seek-first disabled'); - expect(component.prevButtonClasses).toBe('page-item seek-prev disabled'); - expect(component.lastButtonClasses).toBe('page-item seek-end'); - expect(component.nextButtonClasses).toBe('page-item seek-next'); - expect(pageFromToElm.style.display).toBe(''); - }); - - it(`should trigger "onPaginationRefreshed" and expect page from/to being displayed when total items is over 0 and also expect last/next buttons to be disabled when on last page`, () => { - mockFullPagination.pageNumber = 10; - mockFullPagination.pageCount = 10; - mockFullPagination.totalItems = 100; - component.pageNumber = 10; - eventPubSubService.publish('onPaginationRefreshed', mockFullPagination); - const pageFromToElm = document.querySelector('span.page-info-from-to') as HTMLSpanElement; - - expect(component.firstButtonClasses).toBe('page-item seek-first'); - expect(component.prevButtonClasses).toBe('page-item seek-prev'); - expect(component.lastButtonClasses).toBe('page-item seek-end disabled'); - expect(component.nextButtonClasses).toBe('page-item seek-next disabled'); - expect(pageFromToElm.style.display).toBe(''); - }); - - it(`should trigger "onPaginationRefreshed" and expect page from/to NOT being displayed when total items is 0 and also expect all page buttons to be disabled`, () => { - mockFullPagination.pageNumber = 0; - mockFullPagination.totalItems = 0; - component.pageNumber = 0; - eventPubSubService.publish('onPaginationRefreshed', mockFullPagination); - const pageFromToElm = document.querySelector('span.page-info-from-to') as HTMLSpanElement; - - expect(component.firstButtonClasses).toBe('page-item seek-first disabled'); - expect(component.prevButtonClasses).toBe('page-item seek-prev disabled'); - expect(component.lastButtonClasses).toBe('page-item seek-end disabled'); - expect(component.nextButtonClasses).toBe('page-item seek-next disabled'); - expect(pageFromToElm.style.display).toBe('none'); + describe("Integration Tests", () => { + describe.each` + description | paginationServiceStub + ${"Without CursorPagination"} | ${basicPaginationServiceStub} + ${"With CursorPagination"} | ${paginationServiceStubWithCursor} + `(`$description`, ({ description, paginationServiceStub }) => { + beforeAll(() => { + mockFullPagination = { + pageCount: 19, + pageNumber: 2, + pageSize: 5, + pageSizes: [5, 10, 15, 20], + totalItems: 95, + dataFrom: 10, + dataTo: 15, + }; + }); + + beforeEach(() => { + jest.spyOn(SharedService.prototype, 'slickGrid', 'get').mockReturnValue(gridStub); + jest.spyOn(paginationServiceStub, 'getFullPagination').mockReturnValue(mockFullPagination); + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(mockGridOptions); + div = document.createElement('div'); + document.body.appendChild(div); + sharedService = new SharedService(); + eventPubSubService = new EventPubSubService(); + translateService = new TranslateServiceStub(); + + component = new SlickPaginationComponent(paginationServiceStub, eventPubSubService, sharedService, translateService); + component.renderPagination(div); + }); + + afterEach(() => { + // clear all the spyOn mocks to not influence next test + jest.clearAllMocks(); + component.dispose(); + }); + + it('should make sure Slick-Pagination is defined', () => { + const paginationElm = document.querySelector('div.pager.slickgrid_123456') as HTMLSelectElement; + + expect(component).toBeTruthy(); + expect(component.constructor).toBeDefined(); + expect(paginationElm).toBeTruthy(); + }); + + it('should create a the Slick-Pagination component in the DOM', () => { + const pageInfoFromTo = document.querySelector('.page-info-from-to') as HTMLSpanElement; + const pageInfoTotalItems = document.querySelector('.page-info-total-items') as HTMLSpanElement; + const itemsPerPage = document.querySelector('.items-per-page') as HTMLSelectElement; + + expect(translateService.getCurrentLanguage()).toBe('en'); + expect(pageInfoFromTo.querySelector('span.item-from')!.ariaLabel).toBe('Page Item From'); // JSDom doesn't support ariaLabel, but we can test attribute this way + expect(pageInfoFromTo.querySelector('span.item-to')!.ariaLabel).toBe('Page Item To'); + expect(pageInfoTotalItems.querySelector('span.total-items')!.ariaLabel).toBe('Total Items'); + expect(removeExtraSpaces(pageInfoFromTo.innerHTML)).toBe('10-15 of '); + expect(removeExtraSpaces(pageInfoTotalItems.innerHTML)).toBe('95 items '); + expect(itemsPerPage.selectedOptions[0].value).toBe('5'); + }); + + it('should call changeToFirstPage() from the View and expect the pagination service to be called with correct method', () => { + const spy = jest.spyOn(paginationServiceStub, 'goToFirstPage'); + + const button = document.querySelector('.icon-seek-first') as HTMLAnchorElement; + button.click(); + mockFullPagination.pageNumber = 1; + mockFullPagination.dataFrom = 1; + mockFullPagination.dataTo = 10; + jest.spyOn(paginationServiceStub, 'dataFrom', 'get').mockReturnValue(mockFullPagination.dataFrom); + jest.spyOn(paginationServiceStub, 'dataTo', 'get').mockReturnValue(mockFullPagination.dataTo); + + + const itemFrom = document.querySelector('.item-from') as HTMLInputElement; + const itemTo = document.querySelector('.item-to') as HTMLInputElement; + + expect(spy).toHaveBeenCalled(); + + if (paginationServiceStub.cursorBased) { + const span = document.querySelector('span.page-number') as HTMLSpanElement; + expect(span.textContent).toBe('1'); + } else { + const input = document.querySelector('input.form-control') as HTMLInputElement; + expect(input.value).toBe('1'); + } + + expect(component.dataFrom).toBe(1); + expect(component.dataTo).toBe(10); + expect(component.itemsPerPage).toBe(5); + expect(itemFrom.textContent).toBe('1'); + expect(itemTo.textContent).toBe('10'); + }); + + it('should call changeToNextPage() from the View and expect the pagination service to be called with correct method', () => { + const spy = jest.spyOn(paginationServiceStub, 'goToNextPage'); + + const button = document.querySelector('.icon-seek-next') as HTMLAnchorElement; + button.click(); + + expect(spy).toHaveBeenCalled(); + }); + + it('should call changeToPreviousPage() from the View and expect the pagination service to be called with correct method', () => { + mockFullPagination.pageNumber = 2; + const spy = jest.spyOn(paginationServiceStub, 'goToPreviousPage'); + + const button = document.querySelector('.icon-seek-prev') as HTMLAnchorElement; + button.click(); + + expect(spy).toHaveBeenCalled(); + }); + + it('should call changeToLastPage() from the View and expect the pagination service to be called with correct method', () => { + const spy = jest.spyOn(paginationServiceStub, 'goToLastPage'); + + const button = document.querySelector('.icon-seek-end') as HTMLAnchorElement; + button.click(); + + expect(spy).toHaveBeenCalled(); + }); + + it('should change the changeItemPerPage select dropdown and expect the pagination service call a change', () => { + const spy = jest.spyOn(paginationServiceStub, 'changeItemPerPage'); + + const newItemsPerPage = 10; + const select = document.querySelector('select') as HTMLSelectElement; + select.value = `${newItemsPerPage}`; + const mockEvent = new CustomEvent('change', { bubbles: true, detail: { target: { value: newItemsPerPage } } }); + select.dispatchEvent(mockEvent); + + expect(spy).toHaveBeenCalledWith(newItemsPerPage); + }); + + it(`should trigger "onPaginationRefreshed" and expect page from/to being displayed when total items is over 0 and also expect first/prev buttons to be disabled when on page 1`, () => { + mockFullPagination.pageNumber = 1; + mockFullPagination.totalItems = 100; + component.pageNumber = 1; + eventPubSubService.publish('onPaginationRefreshed', mockFullPagination); + const pageFromToElm = document.querySelector('span.page-info-from-to') as HTMLSpanElement; + + expect(component.firstButtonClasses).toBe('page-item seek-first disabled'); + expect(component.prevButtonClasses).toBe('page-item seek-prev disabled'); + expect(component.lastButtonClasses).toBe('page-item seek-end'); + expect(component.nextButtonClasses).toBe('page-item seek-next'); + expect(pageFromToElm.style.display).toBe(''); + }); + + it(`should trigger "onPaginationRefreshed" and expect page from/to being displayed when total items is over 0 and also expect last/next buttons to be disabled when on last page`, () => { + mockFullPagination.pageNumber = 10; + mockFullPagination.pageCount = 10; + mockFullPagination.totalItems = 100; + component.pageNumber = 10; + eventPubSubService.publish('onPaginationRefreshed', mockFullPagination); + const pageFromToElm = document.querySelector('span.page-info-from-to') as HTMLSpanElement; + + expect(component.firstButtonClasses).toBe('page-item seek-first'); + expect(component.prevButtonClasses).toBe('page-item seek-prev'); + expect(component.lastButtonClasses).toBe('page-item seek-end disabled'); + expect(component.nextButtonClasses).toBe('page-item seek-next disabled'); + expect(pageFromToElm.style.display).toBe(''); + }); + + it(`should trigger "onPaginationRefreshed" and expect page from/to NOT being displayed when total items is 0 and also expect all page buttons to be disabled`, () => { + mockFullPagination.pageNumber = 0; + mockFullPagination.totalItems = 0; + component.pageNumber = 0; + eventPubSubService.publish('onPaginationRefreshed', mockFullPagination); + const pageFromToElm = document.querySelector('span.page-info-from-to') as HTMLSpanElement; + + expect(component.firstButtonClasses).toBe('page-item seek-first disabled'); + expect(component.prevButtonClasses).toBe('page-item seek-prev disabled'); + expect(component.lastButtonClasses).toBe('page-item seek-end disabled'); + expect(component.nextButtonClasses).toBe('page-item seek-next disabled'); + expect(pageFromToElm.style.display).toBe('none'); + }); }); }); }); @@ -242,6 +265,10 @@ describe('with different i18n locale', () => { dataTo: 15, }; + const paginationServiceStub = { + ...basicPaginationServiceStub + } as unknown as PaginationService; + beforeEach(() => { mockGridOptions.enableTranslate = true; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(mockGridOptions); From 84076d6ff98bee14601fea0defb44e3942c4f9b6 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Thu, 26 Oct 2023 10:42:38 +1030 Subject: [PATCH 11/32] documentation on why the mockFullPagination reset --- .../pagination-component/src/__tests__/slick-pagination.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pagination-component/src/__tests__/slick-pagination.spec.ts b/packages/pagination-component/src/__tests__/slick-pagination.spec.ts index b27ef46da..6bc0a810f 100644 --- a/packages/pagination-component/src/__tests__/slick-pagination.spec.ts +++ b/packages/pagination-component/src/__tests__/slick-pagination.spec.ts @@ -77,6 +77,7 @@ describe('Slick-Pagination Component', () => { ${"Without CursorPagination"} | ${basicPaginationServiceStub} ${"With CursorPagination"} | ${paginationServiceStubWithCursor} `(`$description`, ({ description, paginationServiceStub }) => { + // Reset mockFullPagination before each entry in the test table beforeAll(() => { mockFullPagination = { pageCount: 19, From 637f9a5afec75dee4a620d89f141f367560659ff Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Thu, 26 Oct 2023 13:26:58 +1030 Subject: [PATCH 12/32] pagination service unit tests --- .../__tests__/pagination.service.spec.ts | 80 ++++++++++- .../common/src/services/pagination.service.ts | 6 +- .../graphql/src/services/graphql.service.ts | 134 ------------------ 3 files changed, 81 insertions(+), 139 deletions(-) diff --git a/packages/common/src/services/__tests__/pagination.service.spec.ts b/packages/common/src/services/__tests__/pagination.service.spec.ts index 5415c479b..4354d9d02 100644 --- a/packages/common/src/services/__tests__/pagination.service.spec.ts +++ b/packages/common/src/services/__tests__/pagination.service.spec.ts @@ -5,6 +5,7 @@ import { SharedService } from '../shared.service'; import { Column, SlickDataView, GridOption, SlickGrid, SlickNamespace, BackendServiceApi, Pagination } from '../../interfaces/index'; import { BackendUtilityService } from '../backendUtility.service'; import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub'; +import { PageInfo } from '../../interfaces/pageInfo.interface'; declare const Slick: SlickNamespace; @@ -61,6 +62,23 @@ const mockGridOption = { } } as GridOption; +const mockGridOptionWithCursorPaginationBackend = { + ...mockGridOption, + backendServiceApi: { + service: mockBackendService, + process: jest.fn(), + options: { + columnDefinitions: [{ id: 'name', field: 'name' }] as Column[], + datasetName: 'user', + isWithCursor: true, + } + }, +} as GridOption; + +const mockPageInfo = { + startCursor: "b", endCursor: "c", hasNextPage: true, hasPreviousPage: true, // b-c simulates page 2 +} as PageInfo; + const gridStub = { autosizeColumns: jest.fn(), getColumnIndex: jest.fn(), @@ -206,6 +224,18 @@ describe('PaginationService', () => { expect(service.getCurrentPageNumber()).toBe(1); expect(spy).toHaveBeenCalledWith(1, undefined); }); + + it('should expect current page to be 1 and "processOnPageChanged" method to be called with cursorArgs when backend service is cursor based', () => { + const spy = jest.spyOn(service, 'processOnPageChanged'); + service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); + service.updatePageInfo(mockPageInfo); + service.goToFirstPage(); + + expect(service.dataFrom).toBe(1); + expect(service.dataTo).toBe(25); + expect(service.getCurrentPageNumber()).toBe(1); + expect(spy).toHaveBeenCalledWith(1, undefined, { first: 25, newPage: 1, pageSize: 25 }); + }); }); describe('goToLastPage method', () => { @@ -220,6 +250,19 @@ describe('PaginationService', () => { expect(service.getCurrentPageNumber()).toBe(4); expect(spy).toHaveBeenCalledWith(4, undefined); }); + + it('should call "goToLastPage" method and expect current page to be last page and "processOnPageChanged" method to be called with cursorArgs when backend service is cursor based', () => { + const spy = jest.spyOn(service, 'processOnPageChanged'); + + service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); + service.updatePageInfo(mockPageInfo); + service.goToLastPage(); + + expect(service.dataFrom).toBe(76); + expect(service.dataTo).toBe(85); + expect(service.getCurrentPageNumber()).toBe(4); + expect(spy).toHaveBeenCalledWith(4, undefined, { last: 25, newPage: 4, pageSize: 25 }); + }); }); describe('goToNextPage method', () => { @@ -235,16 +278,17 @@ describe('PaginationService', () => { expect(spy).toHaveBeenCalledWith(3, undefined); }); - it('should expect page to increment by 1 and "processOnPageChanged" method to be called', () => { + it('should expect page to increment by 1 and "processOnPageChanged" method to be called with cursorArgs when backend service is cursor based', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); - service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); + service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); + service.updatePageInfo(mockPageInfo); service.goToNextPage(); expect(service.dataFrom).toBe(51); expect(service.dataTo).toBe(75); expect(service.getCurrentPageNumber()).toBe(3); - expect(spy).toHaveBeenCalledWith(3, undefined); + expect(spy).toHaveBeenCalledWith(3, undefined, {first: 25, after: "c", newPage: 3, pageSize: 25 }); }); it('should not expect "processOnPageChanged" method to be called when we are already on last page', () => { @@ -262,7 +306,7 @@ describe('PaginationService', () => { }); describe('goToPreviousPage method', () => { - it('should expect page to decrement by 1 and "processOnPageChanged" method to be called', () => { + it('should expect page to decrement by 1 and "processOnPageChanged" method to be called with cursorArgs when backend service is cursor based', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); @@ -274,6 +318,19 @@ describe('PaginationService', () => { expect(spy).toHaveBeenCalledWith(1, undefined); }); + it('should expect page to decrement by 1 and "processOnPageChanged" method to be called', () => { + const spy = jest.spyOn(service, 'processOnPageChanged'); + + service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); + service.updatePageInfo(mockPageInfo); + service.goToPreviousPage(); + + expect(service.dataFrom).toBe(1); + expect(service.dataTo).toBe(25); + expect(service.getCurrentPageNumber()).toBe(1); + expect(spy).toHaveBeenCalledWith(1, undefined, {last: 25, before: "b", newPage: 1, pageSize: 25 }); + }); + it('should not expect "processOnPageChanged" method to be called when we are already on first page', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); mockGridOption.pagination!.pageNumber = 1; @@ -338,6 +395,21 @@ describe('PaginationService', () => { expect(spy).not.toHaveBeenCalled(); expect(output).toBeFalsy(); }); + + it('should not expect "processOnPageChanged" method to be called when backend service is cursor based', async () => { + const spy = jest.spyOn(service, 'processOnPageChanged'); + service.updatePageInfo(mockPageInfo); + service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); + + const output = await service.goToPageNumber(3); + + // stay on current page + expect(service.dataFrom).toBe(26); + expect(service.dataTo).toBe(50); + expect(service.getCurrentPageNumber()).toBe(2); + expect(spy).not.toHaveBeenCalled(); + expect(output).toBeFalsy(); + }); }); describe('processOnPageChanged method', () => { diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 8859d562f..338c19e2e 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -225,7 +225,11 @@ export class PaginationService { } goToPageNumber(pageNumber: number, event?: any, triggerChangeEvent = true): Promise { - console.assert(!this.cursorBased, 'Cursor based navigation cannot navigate to arbitrary page'); + if (this.cursorBased) { + console.assert(true, 'Cursor based navigation cannot navigate to arbitrary page'); + return Promise.resolve(false); + } + const previousPageNumber = this._pageNumber; if (pageNumber < 1) { diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index 3b194eff1..e0db26542 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -537,140 +537,6 @@ export class GraphqlService implements BackendService { this.updateOptions({ paginationOptions }); } - /** - * Update the pagination component with it's new page number and size - * @param newPage - * @param pageSize - */ - // updatePagination(newPage: number, pageSize: number): void; - // updatePagination(cursorArgs: PaginationCursorChangedArgs): void; - // updatePagination(newPageOrCursorArgs: number | PaginationCursorChangedArgs, pageSize?: number) { - // let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption = {}; - - // if (this.options?.isWithCursor) { - // // use cursor based pagination - // const cursorArgs = newPageOrCursorArgs; - // if (cursorArgs instanceof Object) { - // // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments - // paginationOptions = newPageOrCursorArgs as PaginationCursorChangedArgs; - // } else { - // // should not happen unless there is an error in initial configuration - // paginationOptions = this.defaultCursorPaginationOptions; - // } - // } else { - // // use offset based pagination - // const newPage = newPageOrCursorArgs as number; - // this._currentPagination = { - // pageNumber: newPage, - // pageSize: pageSize! - // }; - // paginationOptions = { - // first: pageSize, - // offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 - // }; - // } - - // // if (typeof newPageOrCursorArgs === 'number' || !this.options?.isWithCursor) { - // // const newPage = newPageOrCursorArgs as number; - // // this._currentPagination = { - // // pageNumber: newPage, - // // pageSize: pageSize! - // // }; - // // paginationOptions = { - // // first: pageSize, - // // offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 - // // }; - // // } else { - // // // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments - // // paginationOptions = newPageOrCursorArgs as PaginationCursorChangedArgs; - // // } - - // // else { - // // const newPage = newPageOrCursorArgs as number; - // // this._currentPagination = { - // // pageNumber: newPage, - // // pageSize: pageSize! - // // }; - // // paginationOptions = { - // // first: pageSize, - // // offset: (newPage > 1) ? ((newPage - 1) * pageSize!) : 0 // recalculate offset but make sure the result is always over 0 - // // }; - // // } - - - // // if (this.options && this.options.isWithCursor) { - // // https://dev.to/jackmarchant/offset-and-cursor-pagination-explained-b89 - // // Cursor based pagination does not allow navigation to the middle of the page. - //  // Pagination by page numbers only makes sense in non-relay style pagination - // // Relay style pagination is better suited to infinite scrolling - // // relay pagination - infinte scrolling appending data - // // page1: {startCursor: A, endCursor: B } - // // page2: {startCursor: A, endCursor: C } - // // page3: {startCursor: A, endCursor: D } - - // // non-relay pagination - Getting page chunks - // // page1: {startCursor: A, endCursor: B } - // // page2: {startCursor: B, endCursor: C } - // // page3: {startCursor: C, endCursor: D } - - // // if (cursorArgs) { - // // paginationOptions = cursorArgs; - // // } else { - // // paginationOptions = { - // // first: 0 - // // }; - // // } - - // // can only navigate backwards or forwards if an existing PageInfo is set - // // if (!this.pagination || !this.pageInfo || newPage === 1) { - // // // get the first page - // // paginationOptions = { - // // first: pageSize, - // // }; - // // } - // // else if (newPage === Math.ceil(this.pageInfo.totalCount / pageSize)) { - // // // get the last page - // // paginationOptions = { - // // last: pageSize, - // // }; - // // } - // // else if (this.pageInfo && previousPage) { - // // if (newPage === previousPage) { - // // // stay on same "page", get data from the current cursor position (pageSize may have changed) - // // paginationOptions = { - // // first: pageSize, - // // after: this.pageInfo.startCursor - // // }; - // // } else if(newPage > previousPage) { - // // // navigating forwards - // // https://relay.dev/graphql/connections.htm#sec-Forward-pagination-arguments - // // paginationOptions = { - // // first: pageSize, - // // after: this.pageInfo.endCursor - // // }; - // // } else if(newPage < previousPage) { - // // // navigating backwards - // https://relay.dev/graphql/connections.htm#sec-Backward-pagination-arguments - // // paginationOptions = { - // // last: pageSize, - // // before: this.pageInfo.endCursor - // // }; - // // } - // // } - // // else { - // // paginationOptions = { - // // first: pageSize, - // // }; - // // } - - // // } else { - // // paginationOptions = { - // // first: pageSize, - // // offset: (newPage > 1) ? ((newPage - 1) * pageSize) : 0 // recalculate offset but make sure the result is always over 0 - // // }; - // // } - - // this.updateOptions({ paginationOptions }); - // } - /** * loop through all columns to inspect sorters & update backend service sortingOptions * @param columnFilters From 9966cfac0e2b4ccb4c916efa06e59563072ea813 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Thu, 26 Oct 2023 15:04:36 +1030 Subject: [PATCH 13/32] graphql service tests for cursor pagination --- .../__tests__/graphql.service.spec.ts | 23 +++++++++++++++++++ .../graphql/src/services/graphql.service.ts | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/graphql/src/services/__tests__/graphql.service.spec.ts b/packages/graphql/src/services/__tests__/graphql.service.spec.ts index 594861c8f..e1a095707 100644 --- a/packages/graphql/src/services/__tests__/graphql.service.spec.ts +++ b/packages/graphql/src/services/__tests__/graphql.service.spec.ts @@ -12,6 +12,7 @@ import { MultiColumnSort, OperatorType, Pagination, + PaginationCursorChangedArgs, SharedService, SlickGrid, TranslaterService, @@ -625,6 +626,28 @@ describe('GraphqlService', () => { expect(querySpy).toHaveBeenCalled(); expect(currentPagination).toEqual({ pageNumber: 3, pageSize: 20 }); }); + + describe("CursorBased related scenarios", () => { + describe.each` + description | cursorArgs | expectation + ${"First page"} | ${{ first: 20 }} | ${'query{users(first:20) { totalCount,nodes { id, field1, field2 }, pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}'} + ${"Next Page"} | ${{ first: 20, after: 'a' }} | ${'query{users(first:20, after:"a") { totalCount,nodes { id, field1, field2 }, pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}'} + ${"Previous Page" } | ${{ last: 20, before: 'b' }} | ${'query{users(last:20, before:"b") { totalCount,nodes { id, field1, field2 }, pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}'} + ${"Last Page"} | ${{ last: 20 }} | ${'query{users(last:20) { totalCount,nodes { id, field1, field2 }, pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}'} + `(`$description`, ({ description, cursorArgs, expectation }) => { + it('should return a query with the new pagination and use pagination size options that was passed to service options when it is not provided as argument to "processOnPaginationChanged"', () => { + const querySpy = jest.spyOn(service, 'buildQuery'); + + service.init({ ...serviceOptions, isWithCursor: true }, paginationOptions, gridStub); + const query = service.processOnPaginationChanged(null as any, { newPage: 3, pageSize: 20, ...cursorArgs }); + const currentPagination = service.getCurrentPagination(); + + expect(removeSpaces(query)).toBe(removeSpaces(expectation)); + expect(querySpy).toHaveBeenCalled(); + expect(currentPagination).toEqual({ pageNumber: 3, pageSize: 20 }); + }); + }); + }); }); describe('processOnSortChanged method', () => { diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index e0db26542..523190df7 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -339,7 +339,7 @@ export class GraphqlService implements BackendService { * } * } */ - processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs | PaginationCursorChangedArgs): string { + processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs | (PaginationCursorChangedArgs & PaginationChangedArgs)): string { const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); // if first/last defined on args, then it is a cursor based pagination change From b1f0444f3d8e7d9635d53d69bf7086f3d84cb3de Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Thu, 26 Oct 2023 15:40:28 +1030 Subject: [PATCH 14/32] cleanup --- .../src/interfaces/backendService.interface.ts | 4 ++-- packages/common/src/interfaces/index.ts | 1 + .../src/interfaces/graphqlPageInfo.interface.ts | 13 ------------- .../interfaces/graphqlPaginatedResult.interface.ts | 3 +-- 4 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 packages/graphql/src/interfaces/graphqlPageInfo.interface.ts diff --git a/packages/common/src/interfaces/backendService.interface.ts b/packages/common/src/interfaces/backendService.interface.ts index 2c417d84f..e25f10b49 100644 --- a/packages/common/src/interfaces/backendService.interface.ts +++ b/packages/common/src/interfaces/backendService.interface.ts @@ -51,7 +51,7 @@ export interface BackendService { /** Update the Filters options with a set of new options */ updateFilters?: (columnFilters: ColumnFilters | CurrentFilter[], isUpdatedByPresetOrDynamically: boolean) => void; - /** Update the Pagination component with it's new page number and size. If using cursor based pagination also supply a PageInfo object */ + /** Update the Pagination component with it's new page number and size. If using cursor based pagination, a PageInfo object needs to be supplied */ updatePagination?: (newPage: number, pageSize: number, cursorArgs?: PaginationCursorChangedArgs) => void; /** Update the Sorters options with a set of new options */ @@ -68,7 +68,7 @@ export interface BackendService { processOnFilterChanged: (event: Event | KeyboardEvent | undefined, args: FilterChangedArgs) => string; /** Execute when the pagination changed */ - processOnPaginationChanged: (event: Event | undefined, args: PaginationChangedArgs | PaginationCursorChangedArgs) => string; + processOnPaginationChanged: (event: Event | undefined, args: PaginationChangedArgs | (PaginationCursorChangedArgs & PaginationChangedArgs)) => string; /** Execute when any of the sorters changed */ processOnSortChanged: (event: Event | undefined, args: SingleColumnSort | MultiColumnSort) => string; diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 85ff65e3f..783a1a019 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -118,6 +118,7 @@ export * from './multiColumnSort.interface'; export * from './onEventArgs.interface'; export * from './onValidationErrorResult.interface'; export * from './operatorDetail.interface'; +export * from './pageInfo.interface'; export * from './pagination.interface'; export * from './paginationChangedArgs.interface'; export * from './paginationCursorChangedArgs.interface'; diff --git a/packages/graphql/src/interfaces/graphqlPageInfo.interface.ts b/packages/graphql/src/interfaces/graphqlPageInfo.interface.ts deleted file mode 100644 index 34e44f956..000000000 --- a/packages/graphql/src/interfaces/graphqlPageInfo.interface.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type PageInfo = { -/** Do we have a next page from current cursor position? */ - hasNextPage: boolean; - - /** Do we have a previous page from current cursor position? */ - hasPreviousPage: boolean; - - /** What is the last cursor? */ - endCursor: string; - - /** What is the first cursor? */ - startCursor: string; -}; \ No newline at end of file diff --git a/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts b/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts index 6705a8ea5..07bca662c 100644 --- a/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts +++ b/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts @@ -1,5 +1,4 @@ -import type { Metrics } from '@slickgrid-universal/common'; -import { PageInfo } from './graphqlPageInfo.interface'; +import type { Metrics, PageInfo } from '@slickgrid-universal/common'; export interface GraphqlPaginatedResult { data: { From 6817ad15855f23bbbbf7b7e43215533d20d47517 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Thu, 26 Oct 2023 15:47:54 +1030 Subject: [PATCH 15/32] Documentation --- .../common/src/services/pagination.service.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 338c19e2e..64df4c07a 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -88,6 +88,23 @@ export class PaginationService { } } + /** + * https://dev.to/jackmarchant/offset-and-cursor-pagination-explained-b89 + * Cursor based pagination does not allow navigation to the middle of the page. + * Pagination by page numbers only makes sense in non-relay style pagination + * Relay style pagination is better suited to infinite scrolling + * + * eg + * relay pagination - infinte scrolling appending data + * page1: {startCursor: A, endCursor: B } + * page2: {startCursor: A, endCursor: C } + * page3: {startCursor: A, endCursor: D } + * + * non-relay pagination - Getting page chunks + * page1: {startCursor: A, endCursor: B } + * page2: {startCursor: B, endCursor: C } + * page3: {startCursor: C, endCursor: D } + */ get cursorBased(): boolean { return !!this._backendServiceApi?.options.isWithCursor; } From 91ab89e742ae12a293c85b8fd1afccb49b83190b Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 27 Oct 2023 09:37:14 +1030 Subject: [PATCH 16/32] grammer --- packages/common/src/services/pagination.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 64df4c07a..977b9819d 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -90,12 +90,12 @@ export class PaginationService { /** * https://dev.to/jackmarchant/offset-and-cursor-pagination-explained-b89 - * Cursor based pagination does not allow navigation to the middle of the page. - * Pagination by page numbers only makes sense in non-relay style pagination + * Cursor based pagination does not allow navigation to a page in the middle of a set of pages (eg: LinkedList vs Vector). + * Further, Pagination with page numbers only makes sense in non-relay style pagination * Relay style pagination is better suited to infinite scrolling * * eg - * relay pagination - infinte scrolling appending data + * relay pagination - Infinte scrolling appending data * page1: {startCursor: A, endCursor: B } * page2: {startCursor: A, endCursor: C } * page3: {startCursor: A, endCursor: D } From de4cc8bebde3b11ea47b9129ee1fbe737ea4b415 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 27 Oct 2023 10:04:09 +1030 Subject: [PATCH 17/32] grammer --- packages/pagination-component/src/slick-pagination.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pagination-component/src/slick-pagination.component.ts b/packages/pagination-component/src/slick-pagination.component.ts index d4668842b..e8c11f6e3 100644 --- a/packages/pagination-component/src/slick-pagination.component.ts +++ b/packages/pagination-component/src/slick-pagination.component.ts @@ -286,7 +286,7 @@ export class SlickPaginationComponent { createDomElement('span', { className: 'text-page', textContent: 'Page' }, divElm); divElm.appendChild(document.createTextNode(' ')); if (this.paginationService.cursorBased) { - // cursor based navigation cannot jump to an arbitrary page. Display current page. + // cursor based navigation cannot jump to an arbitrary page. Simply display current page number. createDomElement('span', { className: 'page-number', ariaLabel: 'Page Number', From bd17eb6f88f144b980e3db0016e0ddb63c20f8eb Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 27 Oct 2023 10:11:40 +1030 Subject: [PATCH 18/32] Move _pageInfo to the other instance properties and change to protected --- packages/common/src/services/pagination.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 977b9819d..33d6a226d 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -34,6 +34,7 @@ export class PaginationService { protected _paginationOptions!: Pagination; protected _previousPagination?: Pagination; protected _subscriptions: EventSubscription[] = []; + protected _pageInfo?: PageInfo; /** SlickGrid Grid object */ grid!: SlickGrid; @@ -493,7 +494,6 @@ export class PaginationService { } } - private _pageInfo?: PageInfo; updatePageInfo(pageInfo: PageInfo) { this._pageInfo = pageInfo; } From 6764b87825fbecb4925f04b8034e841291ee6fa3 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 27 Oct 2023 10:43:25 +1030 Subject: [PATCH 19/32] odata shouldn't know about cursors if it doesn't support them (keep an assert statement just in case) --- .../odata/src/services/grid-odata.service.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/odata/src/services/grid-odata.service.ts b/packages/odata/src/services/grid-odata.service.ts index 8f44d982d..bbe345379 100644 --- a/packages/odata/src/services/grid-odata.service.ts +++ b/packages/odata/src/services/grid-odata.service.ts @@ -19,7 +19,6 @@ import type { SharedService, SingleColumnSort, SlickGrid, - PaginationCursorChangedArgs, } from '@slickgrid-universal/common'; import { CaseType, @@ -268,17 +267,14 @@ export class GridOdataService implements BackendService { /* * PAGINATION */ - processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs | PaginationCursorChangedArgs) { - if ('pageSize' in args || 'newPage' in args) { - const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); - this.updatePagination(args.newPage, pageSize); + processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs) { + console.assert('first' in args || 'last' in args, 'cursor based pagination not supported in this service'); - // build the OData query which we will use in the WebAPI callback - return this._odataService.buildQuery(); - } + const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); + this.updatePagination(args.newPage, pageSize); - console.assert(true, 'cursor based pagination not supported in this service'); - return ''; // cursor based pagination not supported in this service + // build the OData query which we will use in the WebAPI callback + return this._odataService.buildQuery(); } /* From 10d0237ee36342c90068658c0c89cd2ebb4fb9cb Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 27 Oct 2023 11:25:56 +1030 Subject: [PATCH 20/32] add to test coverage --- .../__tests__/pagination.service.spec.ts | 19 +++++++++++++++++++ .../odata/src/services/grid-odata.service.ts | 2 -- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/common/src/services/__tests__/pagination.service.spec.ts b/packages/common/src/services/__tests__/pagination.service.spec.ts index 4354d9d02..9e969fc55 100644 --- a/packages/common/src/services/__tests__/pagination.service.spec.ts +++ b/packages/common/src/services/__tests__/pagination.service.spec.ts @@ -236,6 +236,15 @@ describe('PaginationService', () => { expect(service.getCurrentPageNumber()).toBe(1); expect(spy).toHaveBeenCalledWith(1, undefined, { first: 25, newPage: 1, pageSize: 25 }); }); + + it('should expect current page to be 1 and "processOnPageChanged" method NOT to be called', () => { + const spy = jest.spyOn(service, 'processOnPageChanged'); + service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); + service.goToFirstPage(null, false); + + expect(service.getCurrentPageNumber()).toBe(1); + expect(spy).not.toHaveBeenCalled(); + }); }); describe('goToLastPage method', () => { @@ -263,6 +272,16 @@ describe('PaginationService', () => { expect(service.getCurrentPageNumber()).toBe(4); expect(spy).toHaveBeenCalledWith(4, undefined, { last: 25, newPage: 4, pageSize: 25 }); }); + + it('should call "goToLastPage" method and expect current page to be last page and "processOnPageChanged" method NOT to be called', () => { + const spy = jest.spyOn(service, 'processOnPageChanged'); + + service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); + service.goToLastPage(null, false); + + expect(service.getCurrentPageNumber()).toBe(4); + expect(spy).not.toHaveBeenCalledWith(); + }); }); describe('goToNextPage method', () => { diff --git a/packages/odata/src/services/grid-odata.service.ts b/packages/odata/src/services/grid-odata.service.ts index bbe345379..8c99ab84c 100644 --- a/packages/odata/src/services/grid-odata.service.ts +++ b/packages/odata/src/services/grid-odata.service.ts @@ -268,8 +268,6 @@ export class GridOdataService implements BackendService { * PAGINATION */ processOnPaginationChanged(_event: Event | undefined, args: PaginationChangedArgs) { - console.assert('first' in args || 'last' in args, 'cursor based pagination not supported in this service'); - const pageSize = +(args.pageSize || ((this.pagination) ? this.pagination.pageSize : DEFAULT_PAGE_SIZE)); this.updatePagination(args.newPage, pageSize); From 801b78a75685486ad5115f30e64583f4dcd955e1 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 27 Oct 2023 12:48:25 +1030 Subject: [PATCH 21/32] improve coverage --- packages/common/src/services/pagination.service.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 33d6a226d..b05c16c90 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -212,9 +212,8 @@ export class PaginationService { return this.cursorBased && this._pageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage }) : this.processOnPageChanged(this._pageNumber, event); - } else { - return Promise.resolve(this.getFullPagination()); } + return Promise.resolve(this.getFullPagination()); } goToLastPage(event?: any, triggerChangeEvent = true): Promise { @@ -223,9 +222,8 @@ export class PaginationService { return this.cursorBased && this._pageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage }) : this.processOnPageChanged(this._pageNumber, event); - } else { - return Promise.resolve(this.getFullPagination()); } + return Promise.resolve(this.getFullPagination()); } goToNextPage(event?: any, triggerChangeEvent = true): Promise { @@ -271,8 +269,6 @@ export class PaginationService { return this.cursorBased && this._pageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage, before: this._pageInfo.startCursor }) : this.processOnPageChanged(this._pageNumber, event); - } else { - return Promise.resolve(this.getFullPagination()); } } return Promise.resolve(false); From e18e097394bde8d484df98db81df174f4ebecfdd Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 27 Oct 2023 15:59:33 +1030 Subject: [PATCH 22/32] tests for refactored code which created new branches --- .../__tests__/pagination.service.spec.ts | 24 +++++++++++++++++-- .../common/src/services/pagination.service.ts | 2 ++ .../src/slick-pagination.component.ts | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/common/src/services/__tests__/pagination.service.spec.ts b/packages/common/src/services/__tests__/pagination.service.spec.ts index 9e969fc55..4fbc20ade 100644 --- a/packages/common/src/services/__tests__/pagination.service.spec.ts +++ b/packages/common/src/services/__tests__/pagination.service.spec.ts @@ -310,6 +310,16 @@ describe('PaginationService', () => { expect(spy).toHaveBeenCalledWith(3, undefined, {first: 25, after: "c", newPage: 3, pageSize: 25 }); }); + it('should expect page to increment by 1 and "processOnPageChanged" method NOT to be called', () => { + const spy = jest.spyOn(service, 'processOnPageChanged'); + + service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); + service.goToNextPage(null, false); + + expect(service.getCurrentPageNumber()).toBe(3); + expect(spy).not.toHaveBeenCalled(); + }); + it('should not expect "processOnPageChanged" method to be called when we are already on last page', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); mockGridOption.pagination!.pageNumber = 4; @@ -325,7 +335,7 @@ describe('PaginationService', () => { }); describe('goToPreviousPage method', () => { - it('should expect page to decrement by 1 and "processOnPageChanged" method to be called with cursorArgs when backend service is cursor based', () => { + it('should expect page to decrement by 1 and "processOnPageChanged" method to be called', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); @@ -337,7 +347,7 @@ describe('PaginationService', () => { expect(spy).toHaveBeenCalledWith(1, undefined); }); - it('should expect page to decrement by 1 and "processOnPageChanged" method to be called', () => { + it('should expect page to decrement by 1 and "processOnPageChanged" method to be called with cursorArgs when backend service is cursor based', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); @@ -350,6 +360,16 @@ describe('PaginationService', () => { expect(spy).toHaveBeenCalledWith(1, undefined, {last: 25, before: "b", newPage: 1, pageSize: 25 }); }); + it('should expect page to decrement by 1 and "processOnPageChanged" method NOT to be called', () => { + const spy = jest.spyOn(service, 'processOnPageChanged'); + + service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); + service.goToPreviousPage(null, false); + + expect(service.getCurrentPageNumber()).toBe(1); + expect(spy).not.toHaveBeenCalled() + }); + it('should not expect "processOnPageChanged" method to be called when we are already on first page', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); mockGridOption.pagination!.pageNumber = 1; diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index b05c16c90..29442a60b 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -269,6 +269,8 @@ export class PaginationService { return this.cursorBased && this._pageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage, before: this._pageInfo.startCursor }) : this.processOnPageChanged(this._pageNumber, event); + } else { + return Promise.resolve(this.getFullPagination()); } } return Promise.resolve(false); diff --git a/packages/pagination-component/src/slick-pagination.component.ts b/packages/pagination-component/src/slick-pagination.component.ts index e8c11f6e3..860f97941 100644 --- a/packages/pagination-component/src/slick-pagination.component.ts +++ b/packages/pagination-component/src/slick-pagination.component.ts @@ -231,6 +231,7 @@ export class SlickPaginationComponent { } changeToCurrentPage(pageNumber: number) { + console.log("asdfasdf"); this.paginationService.goToPageNumber(+pageNumber); } From eab8e3cda428acd5a154e3d0d0654a2f8865570f Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 27 Oct 2023 15:59:49 +1030 Subject: [PATCH 23/32] remove console statement --- packages/pagination-component/src/slick-pagination.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pagination-component/src/slick-pagination.component.ts b/packages/pagination-component/src/slick-pagination.component.ts index 860f97941..e8c11f6e3 100644 --- a/packages/pagination-component/src/slick-pagination.component.ts +++ b/packages/pagination-component/src/slick-pagination.component.ts @@ -231,7 +231,6 @@ export class SlickPaginationComponent { } changeToCurrentPage(pageNumber: number) { - console.log("asdfasdf"); this.paginationService.goToPageNumber(+pageNumber); } From 1089284d50b815095cc8a30db2b86fe712d26e4d Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Fri, 27 Oct 2023 20:51:58 +1030 Subject: [PATCH 24/32] Restore accidentally deleted test (slightly modified to handle cursor pagination logic) --- .../src/__tests__/slick-pagination.spec.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/pagination-component/src/__tests__/slick-pagination.spec.ts b/packages/pagination-component/src/__tests__/slick-pagination.spec.ts index 6bc0a810f..0589c1f4e 100644 --- a/packages/pagination-component/src/__tests__/slick-pagination.spec.ts +++ b/packages/pagination-component/src/__tests__/slick-pagination.spec.ts @@ -164,6 +164,31 @@ describe('Slick-Pagination Component', () => { expect(itemTo.textContent).toBe('10'); }); + it('should change the page number and expect the pagination service to go to that page (except for cursor based pagination)', () => { + const spy = jest.spyOn(paginationServiceStub, 'goToPageNumber'); + + const newPageNumber = 3; + const input = document.querySelector('input.page-number') as HTMLInputElement; + const span = document.querySelector('span.page-number') as HTMLInputElement; + + const mockEvent = new CustomEvent('change', { bubbles: true, detail: { target: { value: newPageNumber } } }); + if (paginationServiceStub.cursorBased) { + expect(input).toBe(null); + expect(span).not.toBe(null); + + span.dispatchEvent(mockEvent); + expect(spy).not.toHaveBeenCalled(); + } else { + expect(span).toBe(null); + expect(input).not.toBe(null); + + input.value = `${newPageNumber}`; + input.dispatchEvent(mockEvent); + component.pageNumber = newPageNumber; + expect(spy).toHaveBeenCalledWith(newPageNumber); + } + }); + it('should call changeToNextPage() from the View and expect the pagination service to be called with correct method', () => { const spy = jest.spyOn(paginationServiceStub, 'goToNextPage'); From f678371a700a5211cf21743cbc54030e43eac4b8 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Mon, 30 Oct 2023 09:15:03 +1030 Subject: [PATCH 25/32] Rename PageInfo to CursorPageInfo --- .../interfaces/backendService.interface.ts | 2 +- ...terface.ts => cursorPageInfo.interface.ts} | 2 +- packages/common/src/interfaces/index.ts | 2 +- .../__tests__/pagination.service.spec.ts | 17 ++++++++-------- .../common/src/services/pagination.service.ts | 20 +++++++++---------- .../graphqlPaginatedResult.interface.ts | 4 ++-- .../graphql/src/services/graphql.service.ts | 2 +- 7 files changed, 24 insertions(+), 25 deletions(-) rename packages/common/src/interfaces/{pageInfo.interface.ts => cursorPageInfo.interface.ts} (89%) diff --git a/packages/common/src/interfaces/backendService.interface.ts b/packages/common/src/interfaces/backendService.interface.ts index e25f10b49..d85c2c49d 100644 --- a/packages/common/src/interfaces/backendService.interface.ts +++ b/packages/common/src/interfaces/backendService.interface.ts @@ -51,7 +51,7 @@ export interface BackendService { /** Update the Filters options with a set of new options */ updateFilters?: (columnFilters: ColumnFilters | CurrentFilter[], isUpdatedByPresetOrDynamically: boolean) => void; - /** Update the Pagination component with it's new page number and size. If using cursor based pagination, a PageInfo object needs to be supplied */ + /** Update the Pagination component with it's new page number and size. If using cursor based pagination, a CursorPageInfo object needs to be supplied */ updatePagination?: (newPage: number, pageSize: number, cursorArgs?: PaginationCursorChangedArgs) => void; /** Update the Sorters options with a set of new options */ diff --git a/packages/common/src/interfaces/pageInfo.interface.ts b/packages/common/src/interfaces/cursorPageInfo.interface.ts similarity index 89% rename from packages/common/src/interfaces/pageInfo.interface.ts rename to packages/common/src/interfaces/cursorPageInfo.interface.ts index 8023ec2e3..cb45d7989 100644 --- a/packages/common/src/interfaces/pageInfo.interface.ts +++ b/packages/common/src/interfaces/cursorPageInfo.interface.ts @@ -1,4 +1,4 @@ -export interface PageInfo { +export interface CursorPageInfo { /** Do we have a next page from current cursor position? */ hasNextPage: boolean; diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 783a1a019..2fc862f1f 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -36,6 +36,7 @@ export * from './currentPagination.interface'; export * from './currentPinning.interface'; export * from './currentRowSelection.interface'; export * from './currentSorter.interface'; +export * from './cursorPageInfo.interface'; export * from './customFooterOption.interface'; export * from './customTooltipOption.interface'; export * from './dataViewOption.interface'; @@ -118,7 +119,6 @@ export * from './multiColumnSort.interface'; export * from './onEventArgs.interface'; export * from './onValidationErrorResult.interface'; export * from './operatorDetail.interface'; -export * from './pageInfo.interface'; export * from './pagination.interface'; export * from './paginationChangedArgs.interface'; export * from './paginationCursorChangedArgs.interface'; diff --git a/packages/common/src/services/__tests__/pagination.service.spec.ts b/packages/common/src/services/__tests__/pagination.service.spec.ts index 4fbc20ade..45ca7352b 100644 --- a/packages/common/src/services/__tests__/pagination.service.spec.ts +++ b/packages/common/src/services/__tests__/pagination.service.spec.ts @@ -2,10 +2,9 @@ import { of, throwError } from 'rxjs'; import { PaginationService } from './../pagination.service'; import { SharedService } from '../shared.service'; -import { Column, SlickDataView, GridOption, SlickGrid, SlickNamespace, BackendServiceApi, Pagination } from '../../interfaces/index'; +import { Column, CursorPageInfo, SlickDataView, GridOption, SlickGrid, SlickNamespace, BackendServiceApi, Pagination } from '../../interfaces/index'; import { BackendUtilityService } from '../backendUtility.service'; import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub'; -import { PageInfo } from '../../interfaces/pageInfo.interface'; declare const Slick: SlickNamespace; @@ -75,9 +74,9 @@ const mockGridOptionWithCursorPaginationBackend = { }, } as GridOption; -const mockPageInfo = { +const mockCursorPageInfo = { startCursor: "b", endCursor: "c", hasNextPage: true, hasPreviousPage: true, // b-c simulates page 2 -} as PageInfo; +} as CursorPageInfo; const gridStub = { autosizeColumns: jest.fn(), @@ -228,7 +227,7 @@ describe('PaginationService', () => { it('should expect current page to be 1 and "processOnPageChanged" method to be called with cursorArgs when backend service is cursor based', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); - service.updatePageInfo(mockPageInfo); + service.setCursorPageInfo(mockCursorPageInfo); service.goToFirstPage(); expect(service.dataFrom).toBe(1); @@ -264,7 +263,7 @@ describe('PaginationService', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); - service.updatePageInfo(mockPageInfo); + service.setCursorPageInfo(mockCursorPageInfo); service.goToLastPage(); expect(service.dataFrom).toBe(76); @@ -301,7 +300,7 @@ describe('PaginationService', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); - service.updatePageInfo(mockPageInfo); + service.setCursorPageInfo(mockCursorPageInfo); service.goToNextPage(); expect(service.dataFrom).toBe(51); @@ -351,7 +350,7 @@ describe('PaginationService', () => { const spy = jest.spyOn(service, 'processOnPageChanged'); service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); - service.updatePageInfo(mockPageInfo); + service.setCursorPageInfo(mockCursorPageInfo); service.goToPreviousPage(); expect(service.dataFrom).toBe(1); @@ -437,7 +436,7 @@ describe('PaginationService', () => { it('should not expect "processOnPageChanged" method to be called when backend service is cursor based', async () => { const spy = jest.spyOn(service, 'processOnPageChanged'); - service.updatePageInfo(mockPageInfo); + service.setCursorPageInfo(mockCursorPageInfo); service.init(gridStub, mockGridOptionWithCursorPaginationBackend.pagination as Pagination, mockGridOptionWithCursorPaginationBackend.backendServiceApi); const output = await service.goToPageNumber(3); diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 29442a60b..768609e22 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -4,6 +4,7 @@ import { dequal } from 'dequal/lite'; import type { BackendServiceApi, CurrentPagination, + CursorPageInfo, Pagination, PaginationCursorChangedArgs, ServicePagination, @@ -14,7 +15,6 @@ import type { import type { BackendUtilityService } from './backendUtility.service'; import type { SharedService } from './shared.service'; import type { Observable, RxJsFacade } from './rxjsFacade'; -import { PageInfo } from '../interfaces/pageInfo.interface'; // using external non-typed js libraries declare const Slick: SlickNamespace; @@ -34,7 +34,7 @@ export class PaginationService { protected _paginationOptions!: Pagination; protected _previousPagination?: Pagination; protected _subscriptions: EventSubscription[] = []; - protected _pageInfo?: PageInfo; + protected _cursorPageInfo?: CursorPageInfo; /** SlickGrid Grid object */ grid!: SlickGrid; @@ -209,7 +209,7 @@ export class PaginationService { goToFirstPage(event?: any, triggerChangeEvent = true): Promise { this._pageNumber = 1; if (triggerChangeEvent) { - return this.cursorBased && this._pageInfo + return this.cursorBased && this._cursorPageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage }) : this.processOnPageChanged(this._pageNumber, event); } @@ -219,7 +219,7 @@ export class PaginationService { goToLastPage(event?: any, triggerChangeEvent = true): Promise { this._pageNumber = this._pageCount || 1; if (triggerChangeEvent) { - return this.cursorBased && this._pageInfo + return this.cursorBased && this._cursorPageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage }) : this.processOnPageChanged(this._pageNumber, event); } @@ -230,8 +230,8 @@ export class PaginationService { if (this._pageNumber < this._pageCount) { this._pageNumber++; if (triggerChangeEvent) { - return this.cursorBased && this._pageInfo - ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage, after: this._pageInfo.endCursor }) + return this.cursorBased && this._cursorPageInfo + ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage, after: this._cursorPageInfo.endCursor }) : this.processOnPageChanged(this._pageNumber, event); } else { return Promise.resolve(this.getFullPagination()); @@ -266,8 +266,8 @@ export class PaginationService { if (this._pageNumber > 1) { this._pageNumber--; if (triggerChangeEvent) { - return this.cursorBased && this._pageInfo - ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage, before: this._pageInfo.startCursor }) + return this.cursorBased && this._cursorPageInfo + ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage, before: this._cursorPageInfo.startCursor }) : this.processOnPageChanged(this._pageNumber, event); } else { return Promise.resolve(this.getFullPagination()); @@ -492,8 +492,8 @@ export class PaginationService { } } - updatePageInfo(pageInfo: PageInfo) { - this._pageInfo = pageInfo; + setCursorPageInfo(pageInfo: CursorPageInfo) { + this._cursorPageInfo = pageInfo; } updateTotalItems(totalItems: number, triggerChangedEvent = false) { diff --git a/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts b/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts index 07bca662c..89fa4eb57 100644 --- a/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts +++ b/packages/graphql/src/interfaces/graphqlPaginatedResult.interface.ts @@ -1,4 +1,4 @@ -import type { Metrics, PageInfo } from '@slickgrid-universal/common'; +import type { Metrics, CursorPageInfo } from '@slickgrid-universal/common'; export interface GraphqlPaginatedResult { data: { @@ -18,7 +18,7 @@ export interface GraphqlPaginatedResult { } /** Page information of the current cursor, do we have a next page and what is the end cursor? */ - pageInfo?: PageInfo; + pageInfo?: CursorPageInfo; } }; diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index 523190df7..a3ca328d1 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -517,7 +517,7 @@ export class GraphqlService implements BackendService { if (this.options?.isWithCursor) { // use cursor based pagination // when using cursor pagination, expect to be given a PaginationCursorChangedArgs as arguments, - // but still handle the case where it's not (can happen when initial configuration not pre-configured (automatically corrects itself next updatePageInfo() call)) + // but still handle the case where it's not (can happen when initial configuration not pre-configured (automatically corrects itself next setCursorPageInfo() call)) if (cursorArgs && cursorArgs instanceof Object) { // remove pageSize and newPage from cursorArgs, otherwise they get put on the query input string // eslint-disable-next-line From 1696ab60ec1898316753368f4adec0ab1971c10f Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Mon, 30 Oct 2023 09:16:15 +1030 Subject: [PATCH 26/32] rename cursorBased to isCursorBased --- .../common/src/services/pagination.service.ts | 16 ++++++++-------- .../src/__tests__/slick-pagination.spec.ts | 8 ++++---- .../src/slick-pagination.component.ts | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 768609e22..286f25d5b 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -106,7 +106,7 @@ export class PaginationService { * page2: {startCursor: B, endCursor: C } * page3: {startCursor: C, endCursor: D } */ - get cursorBased(): boolean { + get isCursorBased(): boolean { return !!this._backendServiceApi?.options.isWithCursor; } @@ -209,7 +209,7 @@ export class PaginationService { goToFirstPage(event?: any, triggerChangeEvent = true): Promise { this._pageNumber = 1; if (triggerChangeEvent) { - return this.cursorBased && this._cursorPageInfo + return this.isCursorBased && this._cursorPageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage }) : this.processOnPageChanged(this._pageNumber, event); } @@ -219,7 +219,7 @@ export class PaginationService { goToLastPage(event?: any, triggerChangeEvent = true): Promise { this._pageNumber = this._pageCount || 1; if (triggerChangeEvent) { - return this.cursorBased && this._cursorPageInfo + return this.isCursorBased && this._cursorPageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage }) : this.processOnPageChanged(this._pageNumber, event); } @@ -230,7 +230,7 @@ export class PaginationService { if (this._pageNumber < this._pageCount) { this._pageNumber++; if (triggerChangeEvent) { - return this.cursorBased && this._cursorPageInfo + return this.isCursorBased && this._cursorPageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, first: this._itemsPerPage, after: this._cursorPageInfo.endCursor }) : this.processOnPageChanged(this._pageNumber, event); } else { @@ -241,7 +241,7 @@ export class PaginationService { } goToPageNumber(pageNumber: number, event?: any, triggerChangeEvent = true): Promise { - if (this.cursorBased) { + if (this.isCursorBased) { console.assert(true, 'Cursor based navigation cannot navigate to arbitrary page'); return Promise.resolve(false); } @@ -266,7 +266,7 @@ export class PaginationService { if (this._pageNumber > 1) { this._pageNumber--; if (triggerChangeEvent) { - return this.cursorBased && this._cursorPageInfo + return this.isCursorBased && this._cursorPageInfo ? this.processOnPageChanged(this._pageNumber, event, { newPage: this._pageNumber, pageSize: this._itemsPerPage, last: this._itemsPerPage, before: this._cursorPageInfo.startCursor }) : this.processOnPageChanged(this._pageNumber, event); } else { @@ -373,7 +373,7 @@ export class PaginationService { } processOnPageChanged(pageNumber: number, event?: Event | undefined, cursorArgs?: PaginationCursorChangedArgs): Promise { - console.assert(!this.cursorBased || cursorArgs, 'Configured for cursor based pagination - cursorArgs expected'); + console.assert(!this.isCursorBased || cursorArgs, 'Configured for cursor based pagination - cursorArgs expected'); if (this.pubSubService.publish('onBeforePaginationChange', this.getFullPagination()) === false) { this.resetToPreviousPagination(); @@ -400,7 +400,7 @@ export class PaginationService { } if (this._backendServiceApi?.process) { - const query = this.cursorBased && cursorArgs + const query = this.isCursorBased && cursorArgs ? this._backendServiceApi.service.processOnPaginationChanged(event, cursorArgs) : this._backendServiceApi.service.processOnPaginationChanged(event, { newPage: pageNumber, pageSize: itemsPerPage }); diff --git a/packages/pagination-component/src/__tests__/slick-pagination.spec.ts b/packages/pagination-component/src/__tests__/slick-pagination.spec.ts index 0589c1f4e..111a53706 100644 --- a/packages/pagination-component/src/__tests__/slick-pagination.spec.ts +++ b/packages/pagination-component/src/__tests__/slick-pagination.spec.ts @@ -36,7 +36,7 @@ const basicPaginationServiceStub = { pageSize: 5, totalItems: 95, availablePageSizes: [5, 10, 15, 20], - cursorBased: false, + isCursorBased: false, pageInfoTotalItems: jest.fn(), getFullPagination: jest.fn(), goToFirstPage: jest.fn(), @@ -51,7 +51,7 @@ const basicPaginationServiceStub = { const paginationServiceStubWithCursor = { ...basicPaginationServiceStub, - cursorBased: true, + isCursorBased: true, } as unknown as PaginationService [basicPaginationServiceStub, paginationServiceStubWithCursor].forEach(stub => { @@ -149,7 +149,7 @@ describe('Slick-Pagination Component', () => { expect(spy).toHaveBeenCalled(); - if (paginationServiceStub.cursorBased) { + if (paginationServiceStub.isCursorBased) { const span = document.querySelector('span.page-number') as HTMLSpanElement; expect(span.textContent).toBe('1'); } else { @@ -172,7 +172,7 @@ describe('Slick-Pagination Component', () => { const span = document.querySelector('span.page-number') as HTMLInputElement; const mockEvent = new CustomEvent('change', { bubbles: true, detail: { target: { value: newPageNumber } } }); - if (paginationServiceStub.cursorBased) { + if (paginationServiceStub.isCursorBased) { expect(input).toBe(null); expect(span).not.toBe(null); diff --git a/packages/pagination-component/src/slick-pagination.component.ts b/packages/pagination-component/src/slick-pagination.component.ts index e8c11f6e3..60cd0e70d 100644 --- a/packages/pagination-component/src/slick-pagination.component.ts +++ b/packages/pagination-component/src/slick-pagination.component.ts @@ -186,7 +186,7 @@ export class SlickPaginationComponent { this._bindingHelper.addElementBinding(this.currentPagination, 'totalItems', 'span.total-items', 'textContent'); this._bindingHelper.addElementBinding(this.currentPagination, 'pageCount', 'span.page-count', 'textContent'); this._bindingHelper.addElementBinding(this.currentPagination, 'pageSize', 'select.items-per-page', 'value'); - this.paginationService.cursorBased + this.paginationService.isCursorBased ? this._bindingHelper.addElementBinding(this.currentPagination, 'pageNumber', 'span.page-number', 'textContent') : this._bindingHelper.addElementBinding(this.currentPagination, 'pageNumber', 'input.page-number', 'value', 'change', this.changeToCurrentPage.bind(this)); @@ -285,7 +285,7 @@ export class SlickPaginationComponent { const divElm = createDomElement('div', { className: 'slick-page-number' }); createDomElement('span', { className: 'text-page', textContent: 'Page' }, divElm); divElm.appendChild(document.createTextNode(' ')); - if (this.paginationService.cursorBased) { + if (this.paginationService.isCursorBased) { // cursor based navigation cannot jump to an arbitrary page. Simply display current page number. createDomElement('span', { className: 'page-number', From 37f871bebb194a6c65f2e153c3f835d5df60aea4 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Mon, 30 Oct 2023 11:19:44 +1030 Subject: [PATCH 27/32] cleanup nested conditions to be easier to read --- packages/graphql/src/services/graphql.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index a3ca328d1..bbdd23731 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -152,7 +152,7 @@ export class GraphqlService implements BackendService { } else { const paginationOptions = this.options?.paginationOptions; - datasetFilters.first = ((this.options.paginationOptions && this.options.paginationOptions.first) ? this.options.paginationOptions.first : ((this.pagination && this.pagination.pageSize) ? this.pagination.pageSize : null)) || this.defaultPaginationOptions.first; + datasetFilters.first = this.options?.paginationOptions?.first ?? this.pagination?.pageSize ?? this.defaultPaginationOptions.first; datasetFilters.offset = paginationOptions?.hasOwnProperty('offset') ? +(paginationOptions as any)['offset'] : 0; } } From f3962b60289615dc5ee6054fc416c928f6a1842d Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Mon, 30 Oct 2023 11:30:01 +1030 Subject: [PATCH 28/32] Specific es-lint rules to disable --- packages/graphql/src/services/graphql.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index bbdd23731..3a4f50873 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -520,7 +520,7 @@ export class GraphqlService implements BackendService { // but still handle the case where it's not (can happen when initial configuration not pre-configured (automatically corrects itself next setCursorPageInfo() call)) if (cursorArgs && cursorArgs instanceof Object) { // remove pageSize and newPage from cursorArgs, otherwise they get put on the query input string - // eslint-disable-next-line + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-shadow const { pageSize, newPage, ...cursorPaginationOptions } = cursorArgs; paginationOptions = cursorPaginationOptions; } else { From d1d01e69577173491c2180d9b60943a412a5a9fc Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Mon, 30 Oct 2023 11:35:49 +1030 Subject: [PATCH 29/32] change to "import type" --- .../src/interfaces/paginationCursorChangedArgs.interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts b/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts index ad5742697..13c7fdb77 100644 --- a/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts +++ b/packages/common/src/interfaces/paginationCursorChangedArgs.interface.ts @@ -1,4 +1,4 @@ -import { PaginationChangedArgs } from './paginationChangedArgs.interface'; +import type { PaginationChangedArgs } from './paginationChangedArgs.interface'; export interface PaginationCursorChangedArgs extends PaginationChangedArgs { /** Start our page After cursor X */ From 252f5c319c347db73eb68a586df971146fc7955a Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Tue, 31 Oct 2023 14:29:48 +1030 Subject: [PATCH 30/32] E2E tests for cursor pagination --- .../src/examples/example10.html | 15 ++- .../src/examples/example10.ts | 60 ++++++++- test/cypress/e2e/example10.cy.ts | 118 ++++++++++++++++++ 3 files changed, 188 insertions(+), 5 deletions(-) diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example10.html b/examples/vite-demo-vanilla-bundle/src/examples/example10.html index 4fc6cb86e..3fbdb7a7c 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example10.html +++ b/examples/vite-demo-vanilla-bundle/src/examples/example10.html @@ -49,6 +49,20 @@
+ + + + + + + + @@ -68,7 +82,6 @@
-
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example10.ts b/examples/vite-demo-vanilla-bundle/src/examples/example10.ts index dc2652006..75d623dd7 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example10.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example10.ts @@ -1,6 +1,7 @@ import { BindingEventService, Column, + CursorPageInfo, FieldType, Filters, Formatters, @@ -174,7 +175,7 @@ export default class Example10 { { columnId: 'name', direction: 'asc' }, { columnId: 'company', direction: SortDirection.DESC } ], - pagination: { pageNumber: 2, pageSize: 20 } + pagination: { pageNumber: this.isWithCursor ? 1 : 2, pageSize: 20 } // if cursor based, start at page 1 }, backendServiceApi: { service: new GraphqlService(), @@ -185,6 +186,7 @@ export default class Example10 { field: 'userId', value: 123 }], + isWithCursor: this.isWithCursor, // sets pagination strategy, if true requires a call to setPageInfo() when graphql call returns // when dealing with complex objects, we want to keep our field name with double quotes // example with gender: query { users (orderBy:[{field:"gender",direction:ASC}]) {} keepArgumentFieldDoubleQuotes: true @@ -220,6 +222,35 @@ export default class Example10 { * @return Promise */ getCustomerApiCall(_query: string): Promise { + let pageInfo: CursorPageInfo; + if (this.sgb) { + const { paginationService } = this.sgb; + // there seems to a timing issue where when you click "cursor" it requests the data before the pagination-service is initialized... + const pageNumber = (paginationService as any)._initialized ? paginationService.getCurrentPageNumber() : 1; + // In the real world, each node item would be A,B,C...AA,AB,AC, etc and so each page would actually be something like A-T, T-AN + // but for this mock data it's easier to represent each page as + // Page1: A-B + // Page2: B-C + // Page3: C-D + // Page4: D-E + // Page5: E-F + const startCursor = String.fromCharCode('A'.charCodeAt(0) + pageNumber - 1); + const endCursor = String.fromCharCode(startCursor.charCodeAt(0) + 1); + pageInfo = { + hasPreviousPage: paginationService.dataFrom === 0, + hasNextPage: paginationService.dataTo === 100, + startCursor, + endCursor + }; + } else { + pageInfo = { + hasPreviousPage: false, + hasNextPage: true, + startCursor: 'A', + endCursor: 'B' + }; + } + // in your case, you will call your WebAPI function (wich needs to return a Promise) // for the demo purpose, we will call a mock WebAPI function const mockedResult = { @@ -228,14 +259,21 @@ export default class Example10 { data: { [GRAPHQL_QUERY_DATASET_NAME]: { nodes: [], - totalCount: 100 - } - } + totalCount: 100, + pageInfo + }, + }, }; return new Promise(resolve => { setTimeout(() => { this.graphqlQuery = this.gridOptions.backendServiceApi!.service.buildQuery(); + if (this.isWithCursor) { + // When using cursor pagination, the pagination service needs to updated with the PageInfo data from the latest request + // This might be done automatically if using a framework specific slickgrid library + // Note because of this timeout, this may cause race conditions with rapid clicks! + this.sgb?.paginationService.setCursorPageInfo((mockedResult.data[GRAPHQL_QUERY_DATASET_NAME].pageInfo)); + } resolve(mockedResult); }, 150); }); @@ -280,6 +318,20 @@ export default class Example10 { ]); } + setIsWithCursor(newValue: boolean) { + this.isWithCursor = newValue; + + // recreate grid and initiialisations + const parent = document.querySelector(`.grid10`)?.parentElement; + this.dispose(); + if (parent) { + const newGrid10El = document.createElement('div'); + newGrid10El.classList.add('grid10'); + parent.appendChild(newGrid10El); + this.attached(); + } + } + async switchLanguage() { const nextLanguage = (this.selectedLanguage === 'en') ? 'fr' : 'en'; await this.translateService.use(nextLanguage); diff --git a/test/cypress/e2e/example10.cy.ts b/test/cypress/e2e/example10.cy.ts index de5a4d3f7..eb2de3984 100644 --- a/test/cypress/e2e/example10.cy.ts +++ b/test/cypress/e2e/example10.cy.ts @@ -400,6 +400,8 @@ describe('Example 10 - GraphQL Grid', { retries: 1 }, () => { }); }); + + describe('Translate by Language', () => { it('should Clear all Filters & Sorts', () => { cy.contains('Clear all Filter & Sorts').click(); @@ -655,5 +657,121 @@ describe('Example 10 - GraphQL Grid', { retries: 1 }, () => { cy.get('.flatpickr-input') .should('contain.value', 'au'); // date range will contains (y to z) or in French (y au z) }); + + it('should switch locale to English', () => { + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test=selected-locale]') + .should('contain', 'en.json'); + }); + }); + + describe('Cursor Pagination', () => { + it('should re-initialize grid for cursor pagination', () => { + cy.get('[data-test=cursor]').click(); + + // the page number input should be a label now + cy.get('[data-test=page-number-label]').should('exist').should('have.text', '1'); + }); + + it('should change Pagination to the last page', () => { + // Go to first page (if not already there) + cy.get('[data-test=goto-first-page').click(); + + cy.get('.icon-seek-end').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeSpaces($span.text()); // remove all white spaces + expect(text).to.eq(removeSpaces(`query{users(last:20, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"},{field:"name",operator:Contains,value:"JohnDoe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish},pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}`)); + }); + }); + + it('should change Pagination to the first page', () => { + // Go to first page (if not already there) + cy.get('[data-test=goto-last-page').click(); + + cy.get('.icon-seek-first').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeSpaces($span.text()); // remove all white spaces + expect(text).to.eq(removeSpaces(`query{users(first:20, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"},{field:"name",operator:Contains,value:"JohnDoe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish},pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}`)); + }); + }); + + it('should change Pagination to next page and all the way to the last', () => { + // Go to first page (if not already there) + cy.get('[data-test=goto-first-page').click(); + cy.get('[data-test=status]').should('contain', 'finished'); + + // on page 1, click 4 times to get to page 5 (the last page) + cy.wrap([0, 1, 2, 3]).each((el, i) => { + cy.wait(200); // Avoid clicking too fast and hitting race conditions because of the setTimeout in the example page (this timeout should be greater than in the page) + cy.get('.icon-seek-next').click().then(() => { + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + // First page is A-B + // first click is to get page after A-B + // => get first 20 after 'B' + const afterCursor = String.fromCharCode('B'.charCodeAt(0) + i); + + const text = removeSpaces($span.text()); // remove all white spaces + expect(text).to.eq(removeSpaces(`query{users(first:20,after:"${afterCursor}", + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"},{field:"name",operator:Contains,value:"JohnDoe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish},pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}`)); + }); + }); + }); + }); + + it('should change Pagination from the last page all the way to the first', () => { + // Go to last page (if not already there) + cy.get('[data-test=goto-last-page').click(); + + // on page 5 (last page), click 4 times to go to page 1 + cy.wrap([0, 1, 2, 3]).each((el, i) => { + cy.wait(200); // Avoid clicking too fast and hitting race conditions because of the setTimeout in the example page (this timeout should be greater than in the page) + cy.get('.icon-seek-prev').click().then(() => { + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + // Last page is E-F + // first click is to get page before E-F + // => get last 20 before 'E' + const beforeCursor = String.fromCharCode('E'.charCodeAt(0) - i); + + const text = removeSpaces($span.text()); // remove all white spaces + expect(text).to.eq(removeSpaces(`query{users(last:20,before:"${beforeCursor}", + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"},{field:"name",operator:Contains,value:"JohnDoe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish},pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}`)); + }); + }); + }); + }); }); }); From a012424133da4662e5b9049a1fe33c4807e583cc Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Tue, 31 Oct 2023 15:03:36 +1030 Subject: [PATCH 31/32] Remove unused property --- packages/graphql/src/services/graphql.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index 3a4f50873..fcb688dd8 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -54,9 +54,6 @@ export class GraphqlService implements BackendService { first: DEFAULT_ITEMS_PER_PAGE, offset: 0 }; - defaultCursorPaginationOptions: GraphqlCursorPaginationOption = { - first: DEFAULT_ITEMS_PER_PAGE, - }; /** Getter for the Column Definitions */ get columnDefinitions() { From 3805d795c96db35454bc8f4917906d0b26c51844 Mon Sep 17 00:00:00 2001 From: Ronald Van Ryswyk Date: Tue, 31 Oct 2023 15:46:17 +1030 Subject: [PATCH 32/32] fix default props when changing filter with cursor --- packages/common/src/services/pagination.service.ts | 1 + .../graphql/src/services/__tests__/graphql.service.spec.ts | 2 +- packages/graphql/src/services/graphql.service.ts | 7 +------ 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index 286f25d5b..29b66d7f2 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -339,6 +339,7 @@ export class PaginationService { // on a local grid we also need to reset the DataView paging to 1st page this.dataView.setPagingOptions({ pageSize: this._itemsPerPage, pageNum: 0 }); } + this._cursorPageInfo = undefined; this.refreshPagination(true, triggerChangedEvent); } diff --git a/packages/graphql/src/services/__tests__/graphql.service.spec.ts b/packages/graphql/src/services/__tests__/graphql.service.spec.ts index e1a095707..b3589baaa 100644 --- a/packages/graphql/src/services/__tests__/graphql.service.spec.ts +++ b/packages/graphql/src/services/__tests__/graphql.service.spec.ts @@ -509,7 +509,7 @@ describe('GraphqlService', () => { service.init({ datasetName: 'users', isWithCursor: true }, paginationOptions); service.resetPaginationOptions(); - expect(spy).toHaveBeenCalledWith({ paginationOptions: { after: '', before: undefined, last: undefined } }); + expect(spy).toHaveBeenCalledWith({ paginationOptions: { first: 20 } }); }); }); diff --git a/packages/graphql/src/services/graphql.service.ts b/packages/graphql/src/services/graphql.service.ts index fcb688dd8..eda7f9be7 100644 --- a/packages/graphql/src/services/graphql.service.ts +++ b/packages/graphql/src/services/graphql.service.ts @@ -255,12 +255,7 @@ export class GraphqlService implements BackendService { let paginationOptions: GraphqlPaginationOption | GraphqlCursorPaginationOption; if (this.options && this.options.isWithCursor) { - // first, last, after, before - paginationOptions = { - after: '', - before: undefined, - last: undefined - } as GraphqlCursorPaginationOption; + paginationOptions = this.getInitPaginationOptions(); } else { // first, last, offset paginationOptions = ((this.options && this.options.paginationOptions) || this.getInitPaginationOptions()) as GraphqlPaginationOption;