Skip to content

Commit

Permalink
feat: add filterQueryOverride to OData Service (#354)
Browse files Browse the repository at this point in the history
* feat: add `filterQueryOverride` to OData Service
  • Loading branch information
ghiscoding authored Jun 8, 2024
1 parent 7157271 commit 8e53c4b
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 16 deletions.
24 changes: 22 additions & 2 deletions docs/backend-services/OData.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [Usage](#grid-definition--call-of-backendserviceapi)
- [Passing Extra Arguments](#passing-extra-arguments-to-the-query)
- [OData options](#odata-options)
- [Override the filter query](#override-the-filter-query)

### Description
OData Backend Service (for Pagination purposes) to get data from a backend server with the help of OData.
Expand Down Expand Up @@ -224,6 +225,25 @@ Navigations within navigations are also supported. For example `columns: [{ id:
The dataset from the backend is automatically extracted and navigation fields are flattened so the grid can display them and sort/filter just work. The exact property that is used as the dataset depends on the oData version: `d.results` for v2, `results` for v3 and `value` for v4. If needed a custom extractor function can be set through `oDataOptions.datasetExtractor`.
For example if the backend responds with `{ value: [{ id: 1, nav1: { field1: 'x' }, { nav2: { field2: 'y' } } ] }` this will be flattened to `{ value: [{ id: 1, 'nav1/field1': 'x', 'nav2/field2': 'y' } ] }`.
## UI Sample of the OData demo
### Override the filter query
![Slickgrid Server Side](https://github.com/ghiscoding/slickgrid-react/blob/master/screenshots/pagination.png)
Column filters may have a `Custom` operator, that acts as a placeholder for you to define your own logic. To do so, the easiest way is to provide the `filterQueryOverride` callback in the OdataOptions. This method will be called with `BackendServiceFilterQueryOverrideArgs` to let you decide dynamically on how the filter should be assembled.
E.g. you could listen for a specific column and the active OperatorType.custom in order to switch the filter to a matchesPattern SQL LIKE search:
```ts
backendServiceApi: {
options: {
filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValue }) => {
if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
let matchesSearch = (searchValue as string).replace(/\*/g, '.*');
matchesSearch = matchesSearch.slice(0, 1) + '%5E' + matchesSearch.slice(1);
matchesSearch = matchesSearch.slice(0, -1) + '$\'';

return `matchesPattern(${fieldName}, ${matchesSearch})`;
}
},
}
}

```
54 changes: 44 additions & 10 deletions src/examples/slickgrid/Example5.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import BaseSlickGridState from './state-slick-grid-base';

const defaultPageSize = 20;
const sampleDataRoot = 'assets/data';
const CARET_HTML_ESCAPED = '%5E';
const PERCENT_HTML_ESCAPED = '%25';

interface Status { text: string, class: string }

Expand Down Expand Up @@ -103,6 +105,10 @@ export default class Example5 extends React.Component<Props, State> {
hideInFilterHeaderRow: false,
hideInColumnTitleRow: true
},
compoundOperatorAltTexts: {
// where '=' is any of the `OperatorString` type shown above
text: { 'Custom': { operatorAlt: '%%', descAlt: 'SQL Like' } },
},
enableCellNavigation: true,
enableFiltering: true,
enableCheckboxSelector: true,
Expand Down Expand Up @@ -130,6 +136,16 @@ export default class Example5 extends React.Component<Props, State> {
enableCount: this.state.isCountEnabled, // add the count in the OData query, which will return a property named "__count" (v2) or "@odata.count" (v4)
enableSelect: this.state.isSelectEnabled,
enableExpand: this.state.isExpandEnabled,
filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValue }) => {
if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') {
let matchesSearch = (searchValue as string).replace(/\*/g, '.*');
matchesSearch = matchesSearch.slice(0, 1) + CARET_HTML_ESCAPED + matchesSearch.slice(1);
matchesSearch = matchesSearch.slice(0, -1) + '$\'';

return `matchesPattern(${fieldName}, ${matchesSearch})`;
}
return;
},
version: this.state.odataVersion // defaults to 2, the query string is slightly different between OData 2 and 4
},
onError: (error: Error) => {
Expand Down Expand Up @@ -159,7 +175,15 @@ export default class Example5 extends React.Component<Props, State> {
type: FieldType.string,
filterable: true,
filter: {
model: Filters.compoundInput
model: Filters.compoundInput,
compoundOperatorList: [
{ operator: '', desc: 'Contains' },
{ operator: '<>', desc: 'Not Contains' },
{ operator: '=', desc: 'Equals' },
{ operator: '!=', desc: 'Not equal to' },
{ operator: 'a*', desc: 'Starts With' },
{ operator: 'Custom', desc: 'SQL Like' },
],
}
},
{
Expand Down Expand Up @@ -261,6 +285,12 @@ export default class Example5 extends React.Component<Props, State> {
}
if (param.includes('$filter=')) {
const filterBy = param.substring('$filter='.length).replace('%20', ' ');
if (filterBy.includes('matchespattern')) {
const regex = new RegExp(`matchespattern\\(([a-zA-Z]+),\\s'${CARET_HTML_ESCAPED}(.*?)'\\)`, 'i');
const filterMatch = filterBy.match(regex) || [];
const fieldName = filterMatch[1].trim();
(columnFilters as any)[fieldName] = { type: 'matchespattern', term: '^' + filterMatch[2].trim() };
}
if (filterBy.includes('contains')) {
const filterMatch = filterBy.match(/contains\(([a-zA-Z\/]+),\s?'(.*?)'/);
const fieldName = filterMatch![1].trim();
Expand Down Expand Up @@ -358,16 +388,20 @@ export default class Example5 extends React.Component<Props, State> {
col = filterTerm;
}
if (filterTerm) {
const [term1, term2] = Array.isArray(searchTerm) ? searchTerm : [searchTerm];

switch (filterType) {
case 'eq': return filterTerm.toLowerCase() === searchTerm;
case 'ne': return filterTerm.toLowerCase() !== searchTerm;
case 'le': return filterTerm.toLowerCase() <= searchTerm;
case 'lt': return filterTerm.toLowerCase() < searchTerm;
case 'gt': return filterTerm.toLowerCase() > searchTerm;
case 'ge': return filterTerm.toLowerCase() >= searchTerm;
case 'ends': return filterTerm.toLowerCase().endsWith(searchTerm);
case 'starts': return filterTerm.toLowerCase().startsWith(searchTerm);
case 'substring': return filterTerm.toLowerCase().includes(searchTerm);
case 'eq': return filterTerm.toLowerCase() === term1;
case 'ne': return filterTerm.toLowerCase() !== term1;
case 'le': return filterTerm.toLowerCase() <= term1;
case 'lt': return filterTerm.toLowerCase() < term1;
case 'gt': return filterTerm.toLowerCase() > term1;
case 'ge': return filterTerm.toLowerCase() >= term1;
case 'ends': return filterTerm.toLowerCase().endsWith(term1);
case 'starts': return filterTerm.toLowerCase().startsWith(term1);
case 'starts+ends': return filterTerm.toLowerCase().startsWith(term1) && filterTerm.toLowerCase().endsWith(term2);
case 'substring': return filterTerm.toLowerCase().includes(term1);
case 'matchespattern': return new RegExp((term1 as string).replace(new RegExp(PERCENT_HTML_ESCAPED, 'g'), '.*'), 'i').test(filterTerm);
}
}
});
Expand Down
22 changes: 22 additions & 0 deletions test/cypress/e2e/example05.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,28 @@ describe('Example 5 - OData Grid', () => {
.should('have.length', 1);
});

it('should perform filterQueryOverride when operator "%%" is selected', () => {
cy.get('.search-filter.filter-name select').find('option').last().then((element) => {
cy.get('.search-filter.filter-name select').select(element.val());
});

cy.get('.search-filter.filter-name')
.find('input')
.clear()
.type('Jo%yn%er');

// wait for the query to finish
cy.get('[data-test=status]').should('contain', 'finished');

cy.get('[data-test=odata-query-result]')
.should(($span) => {
expect($span.text()).to.eq(`$count=true&$top=10&$filter=(matchesPattern(Name, '%5EJo%25yn%25er$'))`);
});

cy.get('.slick-row')
.should('have.length', 1);
});

it('should click on Set Dynamic Filter and expect query and filters to be changed', () => {
cy.get('[data-test=set-dynamic-filter]')
.click();
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compileOnSave": false,
"compilerOptions": {
"target": "es2018",
"target": "es2022",
"module": "esnext",
"sourceMap": true,
"allowSyntheticDefaultImports": true,
Expand All @@ -17,7 +17,7 @@
"strict": true,
"jsx": "react",
"lib": [
"es2018",
"es2022",
"dom"
],
"typeRoots": [
Expand Down
4 changes: 2 additions & 2 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ module.exports = ({ production } = {}) => ({
{
test: /\.[jt]sx?$/,
loader: 'esbuild-loader',
options: { target: 'es2015' }
options: { target: 'es2022' }
},
{ test: /\.(sass|scss)$/, use: ['style-loader', 'css-loader', 'sass-loader'], issuer: /\.[tj]sx?$/i },
{ test: /\.(sass|scss)$/, use: ['css-loader', 'sass-loader'], issuer: /\.html?$/i },
Expand All @@ -75,7 +75,7 @@ module.exports = ({ production } = {}) => ({
optimization: {
minimizer: [
new EsbuildPlugin({
target: 'es2015',
target: 'es2022',
format: 'iife',
css: true,
})
Expand Down

0 comments on commit 8e53c4b

Please sign in to comment.