Skip to content
This repository was archived by the owner on Jun 1, 2025. It is now read-only.

Commit e936f33

Browse files
Ghislain BeaulacGhislain Beaulac
authored andcommitted
fix(odata): use contains with OData version 4
- defaults to OData version 2 (which uses substringof)
1 parent e96ed12 commit e936f33

File tree

6 files changed

+109
-32
lines changed

6 files changed

+109
-32
lines changed
Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
<div class="container-fluid">
22
<h2>{{title}}</h2>
3-
<div class="subtitle row"
4-
[innerHTML]="subTitle"></div>
3+
<div class="subtitle row" [innerHTML]="subTitle"></div>
54

65
<div class="row">
76
<div class="col-sm-4">
8-
<div [class]="status.class"
9-
role="alert"
10-
data-test="status">
7+
<div [class]="status.class" role="alert" data-test="status">
118
<strong>Status: </strong> {{status.text}}
129
<span [hidden]="!processing">
1310
<i class="fa fa-refresh fa-spin fa-lg fa-fw"></i>
@@ -19,17 +16,25 @@ <h2>{{title}}</h2>
1916
</span>
2017
</div>
2118
<div class="col-sm-8">
22-
<div class="alert alert-info"
23-
data-test="alert-odata-query">
19+
<div class="alert alert-info" data-test="alert-odata-query">
2420
<strong>OData Query:</strong> <span data-test="odata-query-result">{{odataQuery}}</span>
2521
</div>
22+
<label>OData Version: </label>
23+
<span data-test="radioVersion">
24+
<label class="radio-inline control-label" for="radio2">
25+
<input type="radio" name="inlineRadioOptions" data-test="version2" id="radio2" checked [value]="2"
26+
(change)="setOdataVersion(2)"> 2
27+
</label>
28+
<label class="radio-inline control-label" for="radio4">
29+
<input type="radio" name="inlineRadioOptions" data-test="version4" id="radio4" [value]="4"
30+
(change)="setOdataVersion(4)"> 4
31+
</label>
32+
</span>
2633
</div>
2734
</div>
2835

29-
<angular-slickgrid gridId="grid5"
30-
[columnDefinitions]="columnDefinitions"
31-
[gridOptions]="gridOptions"
32-
[dataset]="dataset"
33-
(onGridStateChanged)="gridStateChanged($event)">
36+
<angular-slickgrid gridId="grid5" [columnDefinitions]="columnDefinitions" [gridOptions]="gridOptions"
37+
[dataset]="dataset" (onGridStateChanged)="gridStateChanged($event)"
38+
(onAngularGridCreated)="angularGridReady($event)">
3439
</angular-slickgrid>
3540
</div>

src/app/examples/grid-odata.component.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Component, OnInit, Injectable } from '@angular/core';
22
import { HttpClient } from '@angular/common/http';
33
import {
4+
AngularGridInstance,
45
Column,
56
FieldType,
67
Filters,
@@ -35,17 +36,23 @@ export class GridOdataComponent implements OnInit {
3536
<li>You can also preload a grid with certain "presets" like Filters / Sorters / Pagination <a href="https://github.com/ghiscoding/Angular-Slickgrid/wiki/Grid-State-&-Preset" target="_blank">Wiki - Grid Preset</a>
3637
</ul>
3738
`;
39+
angularGrid: AngularGridInstance;
3840
columnDefinitions: Column[];
3941
gridOptions: GridOption;
4042
dataset = [];
4143
statistics: Statistic;
4244

45+
odataVersion = 2;
4346
odataQuery = '';
4447
processing = true;
4548
status = { text: 'processing...', class: 'alert alert-danger' };
4649

4750
constructor(private http: HttpClient) { }
4851

52+
angularGridReady(angularGrid: AngularGridInstance) {
53+
this.angularGrid = angularGrid;
54+
}
55+
4956
ngOnInit(): void {
5057
this.columnDefinitions = [
5158
{
@@ -98,6 +105,7 @@ export class GridOdataComponent implements OnInit {
98105
},
99106
backendServiceApi: {
100107
service: new GridOdataService(),
108+
options: { version: this.odataVersion }, // defaults to 2, the query string is slightly different between OData 2 and 4
101109
preProcess: () => this.displaySpinner(true),
102110
process: (query) => this.getCustomerApiCall(query),
103111
postProcess: (response) => {
@@ -161,37 +169,30 @@ export class GridOdataComponent implements OnInit {
161169
}
162170
if (param.includes('$filter=')) {
163171
const filterBy = param.substring('$filter='.length).replace('%20', ' ');
172+
if (filterBy.includes('contains')) {
173+
const filterMatch = filterBy.match(/contains\(([a-zA-Z\/]+),\s?'(.*?)'/);
174+
const fieldName = filterMatch[2].trim();
175+
columnFilters[fieldName] = { type: 'substring', term: filterMatch[2].trim() };
176+
}
164177
if (filterBy.includes('substringof')) {
165178
const filterMatch = filterBy.match(/substringof\('(.*?)',([a-zA-Z ]*)/);
166179
const fieldName = filterMatch[2].trim();
167-
columnFilters[fieldName] = {
168-
type: 'substring',
169-
term: filterMatch[1].trim()
170-
};
180+
columnFilters[fieldName] = { type: 'substring', term: filterMatch[1].trim() };
171181
}
172182
if (filterBy.includes('eq')) {
173183
const filterMatch = filterBy.match(/([a-zA-Z ]*) eq '(.*?)'/);
174184
const fieldName = filterMatch[1].trim();
175-
columnFilters[fieldName] = {
176-
type: 'equal',
177-
term: filterMatch[2].trim()
178-
};
185+
columnFilters[fieldName] = { type: 'equal', term: filterMatch[2].trim() };
179186
}
180187
if (filterBy.includes('startswith')) {
181188
const filterMatch = filterBy.match(/startswith\(([a-zA-Z ]*),\s?'(.*?)'/);
182189
const fieldName = filterMatch[1].trim();
183-
columnFilters[fieldName] = {
184-
type: 'starts',
185-
term: filterMatch[2].trim()
186-
};
190+
columnFilters[fieldName] = { type: 'starts', term: filterMatch[2].trim() };
187191
}
188192
if (filterBy.includes('endswith')) {
189193
const filterMatch = filterBy.match(/endswith\(([a-zA-Z ]*),\s?'(.*?)'/);
190194
const fieldName = filterMatch[1].trim();
191-
columnFilters[fieldName] = {
192-
type: 'ends',
193-
term: filterMatch[2].trim()
194-
};
195+
columnFilters[fieldName] = { type: 'ends', term: filterMatch[2].trim() };
195196
}
196197
}
197198
}
@@ -259,4 +260,15 @@ export class GridOdataComponent implements OnInit {
259260
gridStateChanged(gridStateChanges: GridStateChange) {
260261
console.log('Client sample, Grid State changed:: ', gridStateChanges);
261262
}
263+
264+
// THIS IS ONLY FOR DEMO PURPOSES DO NOT USE THIS CODE
265+
setOdataVersion(version: number) {
266+
this.odataVersion = version;
267+
const odataService = this.gridOptions.backendServiceApi.service;
268+
// @ts-ignore
269+
odataService.updateOptions({ version: this.odataVersion });
270+
odataService.clearFilters();
271+
this.angularGrid.filterService.clearFilters();
272+
return true;
273+
}
262274
}

src/app/modules/angular-slickgrid/models/odataOption.interface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export interface OdataOption extends BackendServiceOption {
2626
/** Sorting string (or array of string) that must be a valid OData string */
2727
orderBy?: string | string[];
2828

29+
/** OData version number (the query string is different between versions) */
30+
version?: number;
31+
2932
/** When accessed as an object */
3033
[key: string]: any;
3134
}

src/app/modules/angular-slickgrid/services/__tests__/grid-odata.service.spec.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ describe('GridOdataService', () => {
252252
const mockColumn = { id: 'gender', field: 'gender' } as Column;
253253
const mockColumnName = { id: 'firstName', field: 'firstName' } as Column;
254254
const mockColumnFilter = { columnDef: mockColumn, columnId: 'gender', operator: 'EQ', searchTerms: ['female'] } as ColumnFilter;
255-
const mockColumnFilterName = { columnDef: mockColumnName, columnId: 'firstName', operator: 'startsWith', searchTerms: ['John'] } as ColumnFilter;
255+
const mockColumnFilterName = { columnDef: mockColumnName, columnId: 'firstName', operator: 'StartsWith', searchTerms: ['John'] } as ColumnFilter;
256256
const mockFilterChangedArgs = {
257257
columnDef: mockColumn,
258258
columnId: 'gender',
@@ -272,7 +272,7 @@ describe('GridOdataService', () => {
272272
expect(resetSpy).toHaveBeenCalled();
273273
expect(currentFilters).toEqual([
274274
{ columnId: 'gender', operator: 'EQ', searchTerms: ['female'] },
275-
{ columnId: 'firstName', operator: 'startsWith', searchTerms: ['John'] }
275+
{ columnId: 'firstName', operator: 'StartsWith', searchTerms: ['John'] }
276276
]);
277277
});
278278
});
@@ -721,6 +721,29 @@ describe('GridOdataService', () => {
721721
});
722722
});
723723

724+
describe('updateFilters method with OData version 4', () => {
725+
beforeEach(() => {
726+
serviceOptions.version = 4;
727+
serviceOptions.columnDefinitions = [{ id: 'company', field: 'company' }, { id: 'gender', field: 'gender' }, { id: 'name', field: 'name' }];
728+
});
729+
730+
it('should return a query with a date showing as DateTime as per OData requirement', () => {
731+
const expectation = `$top=10&$filter=(contains(Company, 'abc') and UpdatedDate eq DateTime'2001-02-28T00:00:00Z')`;
732+
const mockColumnCompany = { id: 'company', field: 'company' } as Column;
733+
const mockColumnUpdated = { id: 'updatedDate', field: 'updatedDate', type: FieldType.date } as Column;
734+
const mockColumnFilters = {
735+
company: { columnId: 'company', columnDef: mockColumnCompany, searchTerms: ['abc'], operator: 'Contains' },
736+
updatedDate: { columnId: 'updatedDate', columnDef: mockColumnUpdated, searchTerms: ['2001-02-28'], operator: 'EQ' },
737+
} as ColumnFilters;
738+
739+
service.init(serviceOptions, paginationOptions, gridStub);
740+
service.updateFilters(mockColumnFilters, false);
741+
const query = service.buildQuery();
742+
743+
expect(query).toBe(expectation);
744+
});
745+
});
746+
724747
describe('updateSorters method', () => {
725748
beforeEach(() => {
726749
serviceOptions.columnDefinitions = [{ id: 'company', field: 'company' }, { id: 'gender', field: 'gender' }, { id: 'name', field: 'name' }];

src/app/modules/angular-slickgrid/services/grid-odata.service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,12 +340,15 @@ export class GridOdataService implements BackendService {
340340
} else if (fieldType === FieldType.string) {
341341
// string field needs to be in single quotes
342342
if (operator === '' || operator === OperatorType.contains || operator === OperatorType.notContains) {
343-
searchBy = `substringof('${searchValue}', ${fieldName})`;
343+
if (this._odataService.options.version >= 4) {
344+
searchBy = `contains(${fieldName}, '${searchValue}')`;
345+
} else {
346+
searchBy = `substringof('${searchValue}', ${fieldName})`;
347+
}
344348
if (operator === OperatorType.notContains) {
345349
searchBy = `not ${searchBy}`;
346350
}
347351
} else {
348-
// searchBy = `substringof('${searchValue}', ${fieldNameCased}) ${this.mapOdataOperator(operator)} true`;
349352
searchBy = `${fieldName} ${this.mapOdataOperator(operator)} '${searchValue}'`;
350353
}
351354
} else {

test/cypress/integration/example5.spec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,35 @@ describe('Example 5 - OData Grid', () => {
9595
expect($span.text()).to.eq(`$top=10`);
9696
});
9797
});
98+
99+
it('should use "substringof" when OData version is set to 2', () => {
100+
cy.get('.search-filter.filter-name')
101+
.find('input')
102+
.type('John');
103+
104+
// wait for the query to finish
105+
cy.get('[data-test=status]').should('contain', 'done');
106+
107+
cy.get('[data-test=odata-query-result]')
108+
.should(($span) => {
109+
expect($span.text()).to.eq(`$top=10&$filter=(substringof('John', Name))`);
110+
});
111+
});
112+
113+
it('should use "contains" when OData version is set to 4', () => {
114+
cy.get('[data-test=version4]')
115+
.click();
116+
117+
cy.get('.search-filter.filter-name')
118+
.find('input')
119+
.type('John');
120+
121+
// wait for the query to finish
122+
cy.get('[data-test=status]').should('contain', 'done');
123+
124+
cy.get('[data-test=odata-query-result]')
125+
.should(($span) => {
126+
expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'John'))`);
127+
});
128+
});
98129
});

0 commit comments

Comments
 (0)