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: add filterQueryOverride to OData Service #354

Merged
merged 3 commits into from
Jun 8, 2024
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: 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