Skip to content

Commit

Permalink
feat: update GraphQL demo with cursor pagination (#1104)
Browse files Browse the repository at this point in the history
* feat: update GraphQL demo with cursor pagination
  • Loading branch information
ghiscoding authored Nov 2, 2023
1 parent e55fa67 commit c735465
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 82 deletions.
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@
"new-release": "npm run release, note that yarn is not supported with release-it and will throw an error"
},
"dependencies": {
"@slickgrid-universal/common": "~3.4.0",
"@slickgrid-universal/custom-footer-component": "~3.4.0",
"@slickgrid-universal/empty-warning-component": "~3.4.0",
"@slickgrid-universal/common": "~3.4.2",
"@slickgrid-universal/custom-footer-component": "~3.4.2",
"@slickgrid-universal/empty-warning-component": "~3.4.2",
"@slickgrid-universal/event-pub-sub": "~3.4.0",
"@slickgrid-universal/pagination-component": "~3.4.0",
"@slickgrid-universal/row-detail-view-plugin": "~3.4.0",
"@slickgrid-universal/pagination-component": "~3.4.2",
"@slickgrid-universal/row-detail-view-plugin": "~3.4.2",
"aurelia-event-aggregator": "^1.0.3",
"aurelia-framework": "^1.4.1",
"aurelia-i18n": "^4.0.4",
Expand All @@ -77,13 +77,13 @@
"@fnando/sparkline": "^0.3.10",
"@popperjs/core": "^2.11.8",
"@release-it/conventional-changelog": "^7.0.2",
"@slickgrid-universal/composite-editor-component": "~3.4.0",
"@slickgrid-universal/custom-tooltip-plugin": "~3.4.0",
"@slickgrid-universal/excel-export": "~3.4.0",
"@slickgrid-universal/graphql": "~3.4.0",
"@slickgrid-universal/odata": "~3.4.0",
"@slickgrid-universal/rxjs-observable": "~3.4.0",
"@slickgrid-universal/text-export": "~3.4.0",
"@slickgrid-universal/composite-editor-component": "~3.4.2",
"@slickgrid-universal/custom-tooltip-plugin": "~3.4.2",
"@slickgrid-universal/excel-export": "~3.4.2",
"@slickgrid-universal/graphql": "~3.4.2",
"@slickgrid-universal/odata": "~3.4.2",
"@slickgrid-universal/rxjs-observable": "~3.4.2",
"@slickgrid-universal/text-export": "~3.4.2",
"@types/bluebird": "^3.5.41",
"@types/dompurify": "^3.0.4",
"@types/fnando__sparkline": "^0.3.6",
Expand Down
18 changes: 18 additions & 0 deletions src/examples/slickgrid/example6.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ <h2>
click.delegate="setSortingDynamically()">
Set Sorting Dynamically
</button>
<button class="btn btn-outline-secondary btn-sm" data-test="reset-presets"
click.delegate="resetToOriginalPresets()">
Reset Original Presets
</button>
</div>
</div>

Expand All @@ -52,6 +56,20 @@ <h2>
${selectedLanguage + '.json'}
</span>
</div>

<span style="margin-left: 10px">
<label>Pagination strategy: </label>
<span data-test="radioStrategy">
<label class="radio-inline control-label" for="radioOffset">
<input type="radio" name="inlineRadioOptions" data-test="offset" id="radioOffset" checked value.bind="false"
click.delegate="setIsWithCursor(false)"> Offset
</label>
<label class="radio-inline control-label" for="radioCursor">
<input type="radio" name="inlineRadioOptions" data-test="cursor" id="radioCursor" value.bind="true"
click.delegate="setIsWithCursor(true)"> Cursor
</label>
</span>
</span>
</div>
<br />
<div if.bind="metrics" style="margin: 10px 0px">
Expand Down
86 changes: 81 additions & 5 deletions src/examples/slickgrid/example6.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { GraphqlService, GraphqlPaginatedResult, GraphqlServiceApi, } from '@slickgrid-universal/graphql';
import { GraphqlService, GraphqlPaginatedResult, GraphqlServiceApi, GraphqlServiceOption, } from '@slickgrid-universal/graphql';
import { autoinject } from 'aurelia-framework';
import { I18N } from 'aurelia-i18n';
import * as moment from 'moment-mini';
import {
AureliaGridInstance,
Column,
CursorPageInfo,
FieldType,
Filters,
Formatters,
Expand Down Expand Up @@ -36,15 +37,14 @@ export class Example6 {
<li>You can also preload a grid with certain "presets" like Filters / Sorters / Pagination <a href="https://github.com/ghiscoding/aurelia-slickgrid/wiki/Grid-State-&-Preset" target="_blank">Wiki - Grid Preset</a>
</ul>
`;

isWithCursor = false;
aureliaGrid!: AureliaGridInstance;
columnDefinitions: Column[] = [];
gridOptions!: GridOption;
dataset = [] = [];
metrics!: Metrics;
graphqlService = new GraphqlService();

isWithCursor = false;
graphqlQuery = '';
processing = false;
selectedLanguage: string;
Expand Down Expand Up @@ -168,7 +168,7 @@ export class Example6 {
{ 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: this.graphqlService,
Expand All @@ -179,6 +179,7 @@ export class Example6 {
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
Expand Down Expand Up @@ -215,6 +216,35 @@ export class Example6 {
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getCustomerApiCall(_query: string): Promise<GraphqlPaginatedResult> {
let pageInfo: CursorPageInfo;
if (this.aureliaGrid?.paginationService) {
const { paginationService } = this.aureliaGrid;
// 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 = {
Expand All @@ -223,14 +253,22 @@ export class Example6 {
data: {
[GRAPHQL_QUERY_DATASET_NAME]: {
nodes: [],
totalCount: 100
totalCount: 100,
pageInfo
}
}
};

return new Promise(resolve => {
setTimeout(() => {
this.graphqlQuery = this.graphqlService.buildQuery();
// 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.aureliaGrid?.paginationService?.setCursorPageInfo(mockedResult.data[GRAPHQL_QUERY_DATASET_NAME].pageInfo);
}
resolve(mockedResult);
}, 150);
});
Expand Down Expand Up @@ -275,6 +313,44 @@ export class Example6 {
]);
}

resetToOriginalPresets() {
const presetLowestDay = moment().add(-2, 'days').format('YYYY-MM-DD');
const presetHighestDay = moment().add(20, 'days').format('YYYY-MM-DD');

this.aureliaGrid.filterService.updateFilters([
// you can use OperatorType or type them as string, e.g.: operator: 'EQ'
{ columnId: 'gender', searchTerms: ['male'], operator: OperatorType.equal },
{ columnId: 'name', searchTerms: ['John Doe'], operator: OperatorType.contains },
{ columnId: 'company', searchTerms: ['xyz'], operator: 'IN' },

// use a date range with 2 searchTerms values
{ columnId: 'finish', searchTerms: [presetLowestDay, presetHighestDay], operator: OperatorType.rangeInclusive },
]);
this.aureliaGrid.sortService.updateSorting([
// direction can written as 'asc' (uppercase or lowercase) and/or use the SortDirection type
{ columnId: 'name', direction: 'asc' },
{ columnId: 'company', direction: SortDirection.DESC }
]);
setTimeout(() => {
this.aureliaGrid.paginationService?.changeItemPerPage(20);
this.aureliaGrid.paginationService?.goToPageNumber(2);
});
}

setIsWithCursor(isWithCursor: boolean) {
this.isWithCursor = isWithCursor;
this.resetOptions({ isWithCursor: this.isWithCursor });
return true;
}

private resetOptions(options: Partial<GraphqlServiceOption>) {
const graphqlService = this.gridOptions.backendServiceApi!.service as GraphqlService;
this.aureliaGrid.paginationService!.setCursorBased(options.isWithCursor!);
this.aureliaGrid.paginationService?.goToFirstPage();
graphqlService.updateOptions(options);
this.gridOptions = { ...this.gridOptions };
}

async switchLanguage() {
const nextLanguage = (this.selectedLanguage === 'en') ? 'fr' : 'en';
await this.i18n.setLocale(nextLanguage);
Expand Down
122 changes: 121 additions & 1 deletion test/cypress/e2e/example06.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { removeWhitespaces } from '../plugins/utilities';
const presetLowestDay = moment().add(-2, 'days').format('YYYY-MM-DD');
const presetHighestDay = moment().add(20, 'days').format('YYYY-MM-DD');

describe('Example 6 - GraphQL Grid', { retries: 1 }, () => {
describe('Example 6 - GraphQL Grid', { retries: 0 }, () => {
it('should display Example title', () => {
cy.visit(`${Cypress.config('baseUrl')}/slickgrid/example6`);
cy.get('h2').should('contain', 'Example 6: Grid with Backend GraphQL Service');
Expand Down Expand Up @@ -654,6 +654,126 @@ describe('Example 6 - 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="reset-presets"]').click(); // reset to same original presets
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');
cy.get('[data-test=page-number-input]')
.invoke('val')
.then(text => expect(text).to.eq('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 = removeWhitespaces($span.text()); // remove all white spaces
expect(text).to.eq(removeWhitespaces(`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 = removeWhitespaces($span.text()); // remove all white spaces
expect(text).to.eq(removeWhitespaces(`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 = removeWhitespaces($span.text()); // remove all white spaces
expect(text).to.eq(removeWhitespaces(`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 = removeWhitespaces($span.text()); // remove all white spaces
expect(text).to.eq(removeWhitespaces(`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}}}`));
});
});
});
});
});
});

Loading

0 comments on commit c735465

Please sign in to comment.