Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update GraphQL demo with cursor pagination #1104

Merged
merged 2 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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