From 2a7ebba4c9204390616b60ca57805ce06951c8b5 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Wed, 29 May 2024 20:32:31 -0400 Subject: [PATCH] feat(filters): add a `filterPredicate` option for user customization --- docs/TOC.md | 10 +- .../filters/Input-Filter.md | 181 -------- .../autocomplete-filter-kraaden-lib.md | 2 +- ...ompound-Filters.md => compound-filters.md} | 5 +- .../filters/custom-filter.md | 4 +- .../filters/input-filter.md | 221 ++++++++++ .../{Range-Filters.md => range-filters.md} | 3 +- .../{Select-Filter.md => select-filter.md} | 3 +- ...arch-Filter.md => single-search-filter.md} | 3 +- docs/getting-started/quick-start.md | 4 +- docs/grid-functionalities/Grid-Menu.md | 2 +- .../Grid-State-&-Preset.md | 2 +- .../Localization---Component-Sample.md | 2 +- .../grid-resize-by-content.component.ts | 72 +++- test/cypress/e2e/example31.cy.ts | 406 ++++++++++++------ 15 files changed, 580 insertions(+), 340 deletions(-) delete mode 100644 docs/column-functionalities/filters/Input-Filter.md rename docs/column-functionalities/filters/{Compound-Filters.md => compound-filters.md} (98%) create mode 100644 docs/column-functionalities/filters/input-filter.md rename docs/column-functionalities/filters/{Range-Filters.md => range-filters.md} (98%) rename docs/column-functionalities/filters/{Select-Filter.md => select-filter.md} (99%) rename docs/column-functionalities/filters/{Single-Search-Filter.md => single-search-filter.md} (95%) diff --git a/docs/TOC.md b/docs/TOC.md index 09bd252ca..114fddada 100644 --- a/docs/TOC.md +++ b/docs/TOC.md @@ -22,13 +22,13 @@ * [Select Dropdown Editor (single/multiple)](column-functionalities/editors/select-dropdown-editor.md) * [Filters](column-functionalities/filters/README.md) * [Autocomplete](column-functionalities/filters/autocomplete-filter-kraaden-lib.md) - * [Input Filter (default)](column-functionalities/filters/Input-Filter.md) - * [Select Filter (dropdown)](column-functionalities/filters/Select-Filter.md) - * [Compound Filters](column-functionalities/filters/Compound-Filters.md) - * [Range Filters](column-functionalities/filters/Range-Filters.md) + * [Input Filter (default)](column-functionalities/filters/input-filter.md) + * [Select Filter (dropdown)](column-functionalities/filters/select-filter.md) + * [Compound Filters](column-functionalities/filters/compound-filters.md) + * [Range Filters](column-functionalities/filters/range-filters.md) * [Custom Filter](column-functionalities/filters/custom-filter.md) * [Styling Filled Filters](column-functionalities/filters/styling-filled-filters.md) - * [Single Search Filter](column-functionalities/filters/Single-Search-Filter.md) + * [Single Search Filter](column-functionalities/filters/single-search-filter.md) * [Formatters](column-functionalities/Formatters.md) * [Sorting](column-functionalities/Sorting.md) diff --git a/docs/column-functionalities/filters/Input-Filter.md b/docs/column-functionalities/filters/Input-Filter.md deleted file mode 100644 index f0b3f37ad..000000000 --- a/docs/column-functionalities/filters/Input-Filter.md +++ /dev/null @@ -1,181 +0,0 @@ -#### Index -- [Using an Inclusive Range](#using-an-inclusive-range-default-is-exclusive) -- [Using 2 dots (..) notation](#using-2-dots--notation) -- [Using a Slider Range](#using-a-slider-range-filter) - - [Filter Options](#filter-options) -- [Using a Date Range](#using-a-date-range-filter) -- [Update Filters Dynamically](Input-Filter.md#update-filters-dynamically) - -### Introduction -Range filters allows you to search for a value between 2 min/max values, the 2 most common use case would be to filter between 2 numbers or dates, you can do that with the Slider & Date Range Filters. The range can also be defined as inclusive (`>= 0 and <= 10`) or exclusive (`> 0 and < 10`), the default is exclusive but you can change that, see below for more info. - -### Using an Inclusive Range (default is Exclusive) -By default all the range filters are with exclusive range, which mean between value `x` and `y` but without including them. If you wish to include the `x` and `y` values, you can change that through the `operator` property. - -For example -```ts -// your columns definition -this.columnDefinitions = [ - { - id: 'duration', field: 'duration', name: 'Duration', - filterable: true, - filter: { - model: Filters.input, - operator: OperatorType.rangeInclusive // defaults to exclusive - - // or use the string (case sensitive) - operator: 'RangeInclusive', // defaults to exclusive - } - }, -]; -``` - -## Using 2 dots (..) notation -You can use a regular input filter with the 2 dots (..) notation to represent a range, for example `5..90` would search between the value 5 and 90 (exclusive search unless specified). - -##### Component -```ts -import { Filters, Formatters, GridOption, OperatorType } from '@slickgrid-universal/common'; - -export class GridBasicComponent { - columnDefinitions: Column[]; - gridOptions: GridOption; - dataset: any[]; - - attached(): void { - // your columns definition - this.columnDefinitions = [ - { - id: 'duration', field: 'duration', name: 'Duration', - type: 'number', // you can optionally specify that the data are numbers - filterable: true, - - // input filter is the default, so you can skip this unless you want to specify the `operator` - filter: { - model: 'input', - operator: OperatorType.rangeInclusive // defaults to exclusive - } - }, - ]; - - this.gridOptions = { - // your grid options config - } - } -} -``` - -### Using a Slider Range Filter -The slider range filter is very useful if you can just want to use the mouse to drag/slide a cursor, you can also optionally show/hide the slider values on screen (hiding them would giving you more room without but without the precision). - -##### Component -```ts -import { Filters, Formatters, GridOption, SliderRangeOption, OperatorType } from '@slickgrid-universal/commomn'; - -export class GridBasicComponent { - columnDefinitions: Column[]; - gridOptions: GridOption; - dataset: any[]; - - attached(): void { - // your columns definition - this.columnDefinitions = [ - { - id: 'complete', name: '% Complete', field: 'percentComplete', headerKey: 'PERCENT_COMPLETE', minWidth: 120, - sortable: true, - formatter: Formatters.progressBar, - type: 'number', - filterable: true, - filter: { - model: Filters.sliderRange, - maxValue: 100, // or you can use the filterOptions as well - operator: OperatorType.rangeInclusive, // optional, defaults to exclusive - params: { hideSliderNumbers: false }, // you can hide/show the slider numbers on both side - - // you can also optionally pass any option of the Slider filter - filterOptions: { sliderStartValue: 5 } as SliderRangeOption - } - }, - ]; - - this.gridOptions = { - // your grid options config - } - } -} -``` - -##### Filter Options -All the available options that can be provided as `filterOptions` to your column definitions and you should try to cast your `filterOptions` to the specific interface as much as possible to make sure that you use only valid options of allowed by the targeted filter - -```ts -filter: { - model: Filters.sliderRange, - filterOptions: { - sliderStartValue: 5 - } as SliderOption -} -``` - -#### Grid Option `defaultFilterOptions -You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultFilterOptions` Grid Option. Note that they are set via the filter type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `filterOptions` (also note that each key is already typed with the correct filter option interface), for example - -```ts -this.gridOptions = { - defaultFilterOptions: { - // Note: that `date`, `select` and `slider` are combining both compound & range filters together - date: { range: { min: 'today' } }, - select: { minHeight: 350 }, // typed as MultipleSelectOption - slider: { sliderStartValue: 10 } - } -} -``` - -### Using a Date Range Filter -The date range filter allows you to search data between 2 dates (it uses [Vanilla-Calendar Range](https://vanilla-calendar.pro/) feature). - -##### Component -import { Filters, Formatters, GridOption, OperatorType, VanillaCalendarOption } from '@slickgrid-universal/common'; - -```typescript -export class GridBasicComponent { - columnDefinitions: Column[]; - gridOptions: GridOption; - dataset: any[]; - - attached(): void { - // your columns definition - this.columnDefinitions = [ - { - id: 'finish', name: 'Finish', field: 'finish', headerKey: 'FINISH', - minWidth: 75, width: 120, exportWithFormatter: true, - formatter: Formatters.dateIso, sortable: true, - type: FieldType.date, - filterable: true, - filter: { - model: Filters.dateRange, - - // override any of the Vanilla-Calendar options through "filterOptions" - editorOptions: { range: { min: 'today' } } as VanillaCalendarOption - } - }, - ]; - - this.gridOptions = { - // your grid options config - } - } -} -``` - -#### Filter Options (`VanillaCalendarOption` interface) -All the available options that can be provided as `filterOptions` to your column definitions can be found under this [VanillaCalendarOption interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/vanillaCalendarOption.interface.ts) and you should cast your `filterOptions` with the expected interface to make sure that you use only valid settings of the [Vanilla-Calendar](https://vanilla-calendar.pro/docs/reference/additionally/settings) library. - -```ts -filter: { - model: Filters.compoundDate, - filterOptions: { - range: { min: 'today' } - } as VanillaCalendarOption -} -``` diff --git a/docs/column-functionalities/filters/autocomplete-filter-kraaden-lib.md b/docs/column-functionalities/filters/autocomplete-filter-kraaden-lib.md index b23209550..d689994ca 100644 --- a/docs/column-functionalities/filters/autocomplete-filter-kraaden-lib.md +++ b/docs/column-functionalities/filters/autocomplete-filter-kraaden-lib.md @@ -5,7 +5,7 @@ - [Filter Options (`AutocompleterOption` interface)](#filter-options-autocompleteroption-interface) - [Using Remote API](#using-external-remote-api) - [Force User Input](#autocomplete---force-user-input) -- [Update Filters Dynamically](../../column-functionalities/filters/Input-Filter.md#update-filters-dynamically) +- [Update Filters Dynamically](../../column-functionalities/filters/input-filter.md#update-filters-dynamically) - [Animated Gif Demo](#animated-gif-demo) ### Demo diff --git a/docs/column-functionalities/filters/Compound-Filters.md b/docs/column-functionalities/filters/compound-filters.md similarity index 98% rename from docs/column-functionalities/filters/Compound-Filters.md rename to docs/column-functionalities/filters/compound-filters.md index 8fc95d6ac..1e19774db 100644 --- a/docs/column-functionalities/filters/Compound-Filters.md +++ b/docs/column-functionalities/filters/compound-filters.md @@ -5,9 +5,10 @@ - [Compound Date Filter](#how-to-use-compounddate-filter) - [Compound Operator List (custom list)](#compound-operator-list-custom-list) - [Compound Operator Alternate Texts](#compound-operator-alternate-texts) -- [Filter Complex Object](../Input-Filter.md#how-to-filter-complex-objects) -- [Update Filters Dynamically](../Input-Filter.md#update-filters-dynamically) +- [Filter Complex Object](input-filter.md#how-to-filter-complex-objects) +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) - [How to avoid filtering when only Operator dropdown is changed?](#how-to-avoid-filtering-when-only-operator-dropdown-is-changed) +- [Custom Filter Predicate](input-filter.md#custom-filter-predicate) ### Description Compound filters are a combination of 2 elements (Operator Select + Input Filter) used as a filter on a column. This is very useful to make it obvious to the user that there are Operator available and even more useful with a date picker (`Vanilla-Calendar`). diff --git a/docs/column-functionalities/filters/custom-filter.md b/docs/column-functionalities/filters/custom-filter.md index f98fccf8e..9ad5967c3 100644 --- a/docs/column-functionalities/filters/custom-filter.md +++ b/docs/column-functionalities/filters/custom-filter.md @@ -1,6 +1,6 @@ #### index -- [Filter Complex Object](Input-Filter.md#filter-complex-object) -- [Update Filters Dynamically](Input-Filter.md#update-filters-dynamically) +- [Filter Complex Object](input-filter.md#filter-complex-object) +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) - [Custom Filter with Angular Components](#custom-filter-with-angular-components) ### Demo diff --git a/docs/column-functionalities/filters/input-filter.md b/docs/column-functionalities/filters/input-filter.md new file mode 100644 index 000000000..76bcba13a --- /dev/null +++ b/docs/column-functionalities/filters/input-filter.md @@ -0,0 +1,221 @@ +#### Index +- [Usage](#ui-usage) +- [Filtering with Localization](#filtering-with-localization-i18n) +- [Filter Complex Object](#how-to-filter-complex-objects) +- [Update Filters Dynamically](#update-filters-dynamically) +- [Query Different Field (Filter/Sort)](#query-different-field) +- [Dynamic Query Field](#dynamic-query-field) +- [Debounce/Throttle Text Search (wait for user to stop typing before filtering)](#debouncethrottle-text-search-wait-for-user-to-stop-typing-before-filtering) +- [Ignore Locale Accent in Text Filter/Sorting](#ignore-locale-accent-in-text-filtersorting) +- [Custom Filter Predicate](#custom-filter-predicate) + +### Description +Input filter is the default filter when enabling filters. + +### Demo +[Demo Page](https://ghiscoding.github.io/Angular-Slickgrid/#/clientside) / [Demo Component](https://github.com/ghiscoding/Angular-Slickgrid/blob/master/src/app/examples/grid-clientside.component.ts) + +### UI Usage +All column types support the following operators: (`>`, `>=`, `<`, `<=`, `<>`, `!=`, `=`, `==`, `*`), range filters can also have 1 of these options (`rangeInclusive` or `rangeExclusive`, the inclusive is default) +Example: +- Number type + - `>100` => bigger than 100 + - `<>100` => not include number 100 + - `15..44` => between 15 and 44 (you can also provide option `rangeInclusive` or `rangeExclusive`, inclusive is default) +- Date types + - `>=2001-01-01` => bigger or equal than date `2001-01-01` + - `<02/28/17` => smaller than date `02/28/17` + - `2001-01-01..2002-02-22` => between 2001-01-01 and 2002-02-22 +- String type + - `<>John` (not include the sub-string `John`) + - `John*` => starts with the sub-string `John` + - `*Doe` => ends with the sub-string `Doe` + - `ab..ef` => anything between "af" and "ef" + - refer to ASCII table, it is however case insensitive + +Note that you could do the same functionality with a Compound Filter. + +#### Note +For filters to work properly (default is `string`), make sure to provide a `FieldType` (type is against the dataset, not the Formatter), for example on a Date Filters, we can set the `FieldType` of dateUtc/date (from dataset) can use an extra option of `filterSearchType` to let user filter more easily. For example, with a column having a "UTC Date" coming from the dataset but has a `formatter: Formatters.dateUs`, you can type a date in US format `>02/28/2017`, also when dealing with UTC you have to take the time difference in consideration. + +### How to use Input Filter +Simply set the flag `filterable` to True and and enable the filters in the Grid Options. Here is an example with a full column definition: +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +this.columnDefinitions = [ + { id: 'title', name: 'Title', field: 'title' }, + { id: 'description', name: 'Description', field: 'description', filterable: true } +]; + +// you also need to enable the filters in the Grid Options +this.gridOptions = { + enableFiltering: true +}; +``` + +### Filtering with Localization (i18n) +When using a regular grid with a JSON dataset (that is without using Backend Service API), the filter might not working correctly on cell values that are translated (because it will try to filter against the translation key instead of the actual formatted value). So to bypass this problem, a new extra `params` was created to resolve this, you need to set `useFormatterOuputToFilter` to True and the filter will, has the name suggest, use the output of the Formatter to filter against. Example: +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +this.columnDefinitions = [ + { id: 'title', name: 'Title', field: 'id', + headerKey: 'TITLE', + formatter: this.taskTranslateFormatter, // <-- this could be a custom Formatter or the built-in translateFormatter + filterable: true, + params: { useFormatterOuputToFilter: true } // <-- set this flag to True + }, + { id: 'description', name: 'Description', field: 'description', filterable: true } +]; + +// you also need to enable the filters in the Grid Options +this.gridOptions = { + enableFiltering: true +}; + +// using a custom translate Formatter OR translateFormatter +taskTranslateFormatter: Formatter = (row, cell, value, columnDef, dataContext) => { + return this.i18n.tr('TASK_X', { x: value }); +} +``` + +### How to Filter Complex Objects? +You can filter complex objects using the dot (.) notation inside the `field` property defined in your Columns Definition. + +For example, let say that we have this dataset +```ts +const dataset = [ + { item: 'HP Desktop', buyer: { id: 1234, address: { street: '123 belleville', zip: 123456 }}, + { item: 'Lenovo Mouse', buyer: { id: 456, address: { street: '456 hollywood blvd', zip: 789123 }} +]; +``` + +We can now filter the zip code from the buyer's address using this filter: +```ts +this.columnDefinitions = [ + { + // the zip is a property of a complex object which is under the "buyer" property + // it will use the "field" property to explode (from "." notation) and find the child value + id: 'zip', name: 'ZIP', field: 'buyer.address.zip', filterable: true + // id: 'street', ... +]; +``` + +### Update Filters Dynamically +You can update/change the Filters dynamically (on the fly) via the `updateFilters` method from the `FilterService`. Note that calling this method will override all filters and replace them with the new array of filters provided. For example, you could update the filters from a button click or a select dropdown list with predefined filter set. + +##### View +```html + + + + +``` + +##### Component +```ts +export class Example { + angularGrid: AngularGridInstance; + + angularGridReady(angularGrid: AngularGridInstance) { + this.angularGrid = angularGrid; + } + + setFiltersDynamically() { + // we can Set Filters Dynamically (or different filters) afterward through the FilterService + this.angularGrid.filterService.updateFilters([ + { columnId: 'duration', searchTerms: [2, 25, 48, 50] }, + { columnId: 'complete', searchTerms: [95], operator: '<' }, + { columnId: 'effort-driven', searchTerms: [true] }, + { columnId: 'start', operator: '>=', searchTerms: ['2001-02-28'] }, + ]); + } +``` + +#### Extra Arguments +The `updateFilters` method has 2 extra arguments: +- 2nd argument, defaults to true, is to emit a filter changed event (the GridStateService uses this event) + - optional and defaults to true `updateFilters([], true)` +- 3rd argument is to trigger a backend query (when using a Backend Service like OData/GraphQL), this could be useful when using updateFilters & updateSorting and you wish to only send the backend query once. + - optional and defaults to true `updateFilters([], true, true)` + +### Query Different Field +Sometime you want to display a certain column (let say `countryName`) but you want to filter from a different column (say `countryCode`), in such use case you can use 1 of these 4 optional +- `queryField`: this will affect both the Filter & Sort +- `queryFieldFilter`: this will affect only the Filter +- `queryFieldSorter`: this will affect only the Sort +- `queryFieldNameGetterFn`: dynamically change column to do Filter/Sort (see below) + +### Dynamic Query Field +What if you a field that you only know which field to query only at run time and depending on the item object (`dataContext`)? +We can defined a `queryFieldNameGetterFn` callback that will be executed on each row when Filtering and/or Sorting. +```ts +queryFieldNameGetterFn: (dataContext) => { + // do your logic and return the field name will be queried + // for example let say that we query "profitRatio" when we have a profit else we query "lossRatio" + return dataContext.profit > 0 ? 'profitRatio' : 'lossRatio'; +}, +``` + +### Debounce/Throttle Text Search (wait for user to stop typing before filtering) +When having a large dataset, it might be useful to add a debounce delay so that typing multiple character successively won't affect the search time, you can use the `filterTypingDebounce` grid option for that use case. What it will do is simply wait for the user to finish typing before executing the filter condition, you typically don't want to put this number too high and I find that between 250-500 is a good number. +```ts +this.gridOptions = { + filterTypingDebounce: 250, +}; +``` + +### Ignore Locale Accent in Text Filter/Sorting +You can ignore latin accent (or any other language accent) in text filter via the Grid Option `ignoreAccentOnStringFilterAndSort` flag (default is false) +```ts +this.gridOptions = { + ignoreAccentOnStringFilterAndSort: true, +}; +``` + +### Custom Filter Predicate +You can provide a custom predicate by using the `filterPredicate` when defining your `filter`, the callback will provide you with 2 arguments (`dataContext` and `searchFilterArgs`). The `searchFilterArgs` has a type of `SearchColumnFilter` interface which will provide you more info about the filter itself (like parsed operator, search terms, column definition, column id and type as well). You can see a live demo at [Example 14](https://ghiscoding.github.io/slickgrid-universal/#/example14) and the associated [lines](https://github.com/ghiscoding/slickgrid-universal/blob/1a2c2ff4b72ac3f51b30b1d3d101e84ed9ec9ece/examples/vite-demo-vanilla-bundle/src/examples/example14.ts#L153-L178) of code. + +```ts +this.columnDefinitions = [ + { + id: 'title', name: 'Title', field: 'title', sortable: true, + filterable: true, type: FieldType.string, + filter: { + model: Filters.inputText, + // you can use your own custom filter predicate when built-in filters aren't working for you + // for example the example below will function similarly to an SQL LIKE to answer this SO: https://stackoverflow.com/questions/78471412/angular-slickgrid-filter + filterPredicate: (dataContext, searchFilterArgs) => { + const searchVals = (searchFilterArgs.searchTerms || []) as SearchTerm[]; + if (searchVals?.length) { + const columnId = searchFilterArgs.columnId; + const searchVal = searchVals[0] as string; + const likeMatches = searchVal.split('%'); + if (likeMatches.length > 3) { + // for matches like "%Ta%10%" will return text that starts with "Ta" and ends with "10" (e.g. "Task 10", "Task 110", "Task 210") + const [_, start, end] = likeMatches; + return dataContext[columnId].startsWith(start) && dataContext[columnId].endsWith(end); + } else if (likeMatches.length > 2) { + // for matches like "%Ta%10" will return text that starts with "Ta" and contains "10" (e.g. "Task 10", "Task 100", "Task 101") + const [_, start, contain] = likeMatches; + return dataContext[columnId].startsWith(start) && dataContext[columnId].includes(contain); + } + // for anything else we'll simply expect a Contains + return dataContext[columnId].includes(searchVal); + } + // if we fall here then the value is not filtered out + return true; + }, + }, + }, +]; +``` + +The custom filter predicate above was to answer a Stack Overflow question and will work similarly to an SQL LIKE matcher (it's not perfect and probably requires more work but is enough to demo the usage of a custom filter predicate) + +![image](https://github.com/ghiscoding/slickgrid-universal/assets/643976/3e77774e-3a9f-4ca4-bca7-50a033a4b48d) diff --git a/docs/column-functionalities/filters/Range-Filters.md b/docs/column-functionalities/filters/range-filters.md similarity index 98% rename from docs/column-functionalities/filters/Range-Filters.md rename to docs/column-functionalities/filters/range-filters.md index 6956d18f3..5b6436211 100644 --- a/docs/column-functionalities/filters/Range-Filters.md +++ b/docs/column-functionalities/filters/range-filters.md @@ -4,7 +4,8 @@ - [Using a Slider Range](#using-a-slider-range-filter) - [Filter Options](#filter-options) - [Using a Date Range](#using-a-date-range-filter) -- [Update Filters Dynamically](Input-Filter.md#update-filters-dynamically) +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) +- [Custom Filter Predicate](input-filter.md#custom-filter-predicate) ### Introduction Range filters allows you to search for a value between 2 min/max values, the 2 most common use case would be to filter between 2 numbers or dates, you can do that with the Slider & Date Range Filters. The range can also be defined as inclusive (`>= 0 and <= 10`) or exclusive (`> 0 and < 10`), the default is exclusive but you can change that, see below for more info. diff --git a/docs/column-functionalities/filters/Select-Filter.md b/docs/column-functionalities/filters/select-filter.md similarity index 99% rename from docs/column-functionalities/filters/Select-Filter.md rename to docs/column-functionalities/filters/select-filter.md index cd675fa35..e0957a33a 100644 --- a/docs/column-functionalities/filters/Select-Filter.md +++ b/docs/column-functionalities/filters/select-filter.md @@ -19,7 +19,8 @@ - [Filter Options (`MultipleSelectOption` interface)](#filter-options-multipleselectoption-interface) - [Display shorter selected label text](#display-shorter-selected-label-text) - [Query against a different field](#query-against-another-field-property) -- [Update Filters Dynamically](Input-Filter.md#update-filters-dynamically) +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) +- [Custom Filter Predicate](input-filter.md#custom-filter-predicate) ### Demo [Demo Page](https://ghiscoding.github.io/Angular-Slickgrid/#/clientside) / [Demo Component](https://github.com/ghiscoding/angular-slickgrid/blob/master/src/app/examples/grid-clientside.component.ts) diff --git a/docs/column-functionalities/filters/Single-Search-Filter.md b/docs/column-functionalities/filters/single-search-filter.md similarity index 95% rename from docs/column-functionalities/filters/Single-Search-Filter.md rename to docs/column-functionalities/filters/single-search-filter.md index 0e2538454..2fc64313a 100644 --- a/docs/column-functionalities/filters/Single-Search-Filter.md +++ b/docs/column-functionalities/filters/single-search-filter.md @@ -1,5 +1,6 @@ #### Index -- [Update Filters Dynamically](Input-Filter.md#update-filters-dynamically) +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) +- [Custom Filter Predicate](input-filter.md#custom-filter-predicate) ### Description Some users might want to have 1 main single search for filtering the grid data instead of using multiple column filters. You can see a demo of that below diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index 3000f259e..d66ff7260 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -67,7 +67,7 @@ $row-mouse-hover-color: lightgreen; ### 4. Include it in your App Module (or App Config for Standalone) Below are 2 different setups (with App Module (legacy) or Standalone) but in both cases the `AngularSlickgridModule.forRoot()` is **required**, so make sure to include it. -#### App Module +#### App Module Include `AngularSlickgridModule` in your App Module (`app.module.ts`) **Note** Make sure to add the `forRoot` since it will throw an error in the console when missing. @@ -202,7 +202,7 @@ The last step is really to explore all the pages that are available in this docu - all the `Grid Options` you can take a look at, [Slickgrid-Universal - Grid Options](https://github.com/ghiscoding/angular-slickgrid/blob/master/src/app/modules/angular-slickgrid/models/gridOption.interface.ts) interface - [Formatters](../column-functionalities/Formatters.md) - [Editors](../column-functionalities/Editors.md) -- [Filters](../column-functionalities/filters/Select-Filter.md) +- [Filters](../column-functionalities/filters/select-filter.md) - [Grid Menu](../grid-functionalities/Grid-Menu.md) ... and much more, just explorer the Documentation through the table of content (on your left) diff --git a/docs/grid-functionalities/Grid-Menu.md b/docs/grid-functionalities/Grid-Menu.md index 9204502d6..2782ef71b 100644 --- a/docs/grid-functionalities/Grid-Menu.md +++ b/docs/grid-functionalities/Grid-Menu.md @@ -59,7 +59,7 @@ export class GridDemoComponent { ``` ### Using Grid Presets & Filter SearchTerm(s) -What happens when we use the grid `presets` and a [Filter Default SearchTerms](../column-functionalities/filters/Select-Filter.md#default-search-terms)? In this case, the `presets` will win over filter `searchTerms`. The cascading order of priorities is the following +What happens when we use the grid `presets` and a [Filter Default SearchTerms](../column-functionalities/filters/select-filter.md#default-search-terms)? In this case, the `presets` will win over filter `searchTerms`. The cascading order of priorities is the following 1. Do we have any `presets`? Yes use them, else go to step 2 2. Do we have any Filter `searchTerms`? Yes use them, else go to step 3 3. No `presets` and no `searchTerms`, load grid with default grid & column definitions diff --git a/docs/grid-functionalities/Grid-State-&-Preset.md b/docs/grid-functionalities/Grid-State-&-Preset.md index 4fdb36d7e..544661d1c 100644 --- a/docs/grid-functionalities/Grid-State-&-Preset.md +++ b/docs/grid-functionalities/Grid-State-&-Preset.md @@ -59,7 +59,7 @@ export class GridDemoComponent { ``` ### Using Grid Presets & Filter SearchTerm(s) -What happens when we use the grid `presets` and a [Filter Default SearchTerms](../column-functionalities/filters/Select-Filter.md#default-search-terms)? In this case, the `presets` will win over filter `searchTerms`. The cascading order of priorities is the following +What happens when we use the grid `presets` and a [Filter Default SearchTerms](../column-functionalities/filters/select-filter.md#default-search-terms)? In this case, the `presets` will win over filter `searchTerms`. The cascading order of priorities is the following 1. Do we have any `presets`? Yes use them, else go to step 2 2. Do we have any Filter `searchTerms`? Yes use them, else go to step 3 3. No `presets` and no `searchTerms`, load grid with default grid & column definitions diff --git a/docs/localization/Localization---Component-Sample.md b/docs/localization/Localization---Component-Sample.md index bd121fa16..098739f9f 100644 --- a/docs/localization/Localization---Component-Sample.md +++ b/docs/localization/Localization---Component-Sample.md @@ -64,7 +64,7 @@ taskTranslateFormatter: Formatter = (row, cell, value, columnDef, dataContext) = ``` #### Filtering with Translated cell value (`translateFormatter`) -Since the cell value is to be translated, the regular filtering might behave differently than excepted (it will filter against a translation key instead of filtering against the formatted output which is what we want). If you want to filter against the formatted output (`translateFormatter` or even a custom formatter), you need to fill in the `i18n` property in the Grid Options and set `useFormatterOuputToFilter` to True, for more info please see [Wiki - input filter with localization](../column-functionalities/filters/Input-Filter.md#filtering-with-localization-i18n) +Since the cell value is to be translated, the regular filtering might behave differently than excepted (it will filter against a translation key instead of filtering against the formatted output which is what we want). If you want to filter against the formatted output (`translateFormatter` or even a custom formatter), you need to fill in the `i18n` property in the Grid Options and set `useFormatterOuputToFilter` to True, for more info please see [Wiki - input filter with localization](../column-functionalities/filters/input-filter.md#filtering-with-localization-i18n) #### Using Angular-Slickgrid Formatters.Translate Instead of defining a custom formatter over and over, you could also use the built-in Angular-Slickgrid `Formatters.translate`. However for the formatter to work, you need to provide the `ngx-translate` Service instance, to the Grid Options property `i18n`, as shown below. diff --git a/src/app/examples/grid-resize-by-content.component.ts b/src/app/examples/grid-resize-by-content.component.ts index 73dc4c2b1..cd2eac20b 100644 --- a/src/app/examples/grid-resize-by-content.component.ts +++ b/src/app/examples/grid-resize-by-content.component.ts @@ -2,7 +2,25 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { ExcelExportService } from '@slickgrid-universal/excel-export'; -import { AngularGridInstance, Column, GridOption, Filters, Formatter, LongTextEditorOption, FieldType, Editors, Formatters, AutocompleterOption, EditCommand, formatNumber, SortComparers, SlickGrid, SlickGlobalEditorLock, VanillaCalendarOption } from '../modules/angular-slickgrid'; +import { + type AngularGridInstance, + type Column, + type GridOption, + Filters, + type Formatter, + type LongTextEditorOption, + FieldType, + Editors, + Formatters, + type AutocompleterOption, + type EditCommand, + formatNumber, + SortComparers, + type SlickGrid, + SlickGlobalEditorLock, + type VanillaCalendarOption, + type SearchTerm +} from '../modules/angular-slickgrid'; const URL_COUNTRIES_COLLECTION = 'assets/data/countries.json'; @@ -13,7 +31,8 @@ const URL_COUNTRIES_COLLECTION = 'assets/data/countries.json'; * @param {*} grid - slickgrid grid object * @returns {boolean} isEditable */ -function checkItemIsEditable(dataContext: any, columnDef: Column, grid: SlickGrid) { +function checkItemIsEditable(dataContext: any, + columnDef: Column, grid: SlickGrid) { const gridOptions = grid && grid.getOptions && grid.getOptions(); const hasEditor = columnDef.editor; const isGridEditable = gridOptions.editable; @@ -94,8 +113,51 @@ export class GridResizeByContentComponent implements OnInit { resizeCharWidthInPx: 7.6, resizeCalcWidthRatio: 1, resizeMaxWidthThreshold: 200, - cssClass: 'text-uppercase fw-bold', columnGroup: 'Common Factor', - filterable: true, filter: { model: Filters.compoundInputText }, + columnGroup: 'Common Factor', + cssClass: 'text-uppercase fw-bold', + filterable: true, + filter: { + model: Filters.inputText, + // you can use your own custom filter predicate when built-in filters aren't working for you + // for example the example below will function similarly to an SQL LIKE to answer this SO: https://stackoverflow.com/questions/78471412/angular-slickgrid-filter + filterPredicate: (dataContext, searchFilterArgs) => { + const searchVals = (searchFilterArgs.parsedSearchTerms || []) as SearchTerm[]; + if (searchVals?.length) { + const columnId = searchFilterArgs.columnId; + const searchVal = searchVals[0] as string; + const cellValue = dataContext[columnId].toLowerCase(); + const results = searchVal.matchAll(/^%([^%\r\n]+)[^%\r\n]*$|(.*)%(.+)%(.*)|(.+)%(.+)|([^%\r\n]+)%$/gi); + const arrayOfMatches = Array.from(results); + const matches = arrayOfMatches.length ? arrayOfMatches[0] : []; + const [_, endW, containSW, contain, containEndW, comboSW, comboEW, startW] = matches; + + if (endW) { + // example: "%001" ends with A + return cellValue.endsWith(endW.toLowerCase()); + } else if (containSW && contain) { + // example: "%Ti%001", contains A + ends with B + return cellValue.startsWith(containSW.toLowerCase()) && cellValue.includes(contain.toLowerCase()); + } else if (contain && containEndW) { + // example: "%Ti%001", contains A + ends with B + return cellValue.includes(contain) && cellValue.endsWith(containEndW.toLowerCase()); + } else if (contain && !containEndW) { + // example: "%Ti%", contains A anywhere + return cellValue.includes(contain.toLowerCase()); + } else if (comboSW && comboEW) { + // example: "Ti%001", combo starts with A + ends with B + return cellValue.startsWith(comboSW.toLowerCase()) && cellValue.endsWith(comboEW.toLowerCase()); + } else if (startW) { + // example: "Ti%", starts with A + return cellValue.startsWith(startW.toLowerCase()); + } + // anything else + return cellValue.includes(searchVal.toLowerCase()); + } + + // if we fall here then the value is not filtered out + return true; + }, + }, editor: { model: Editors.longText, required: true, alwaysSaveOnEnterKey: true, maxLength: 12, @@ -226,7 +288,7 @@ export class GridResizeByContentComponent implements OnInit { }, filter: { model: Filters.inputText, - // placeholder: '🔎︎ search city', + // placeholder: '🔎︎ search product', type: FieldType.string, queryField: 'product.itemName', } diff --git a/test/cypress/e2e/example31.cy.ts b/test/cypress/e2e/example31.cy.ts index 735ee5a8a..a9ff81f40 100644 --- a/test/cypress/e2e/example31.cy.ts +++ b/test/cypress/e2e/example31.cy.ts @@ -8,180 +8,314 @@ describe('Example 31 - Columns Resize by Content', () => { }); }); - it('should display Example title', () => { - cy.visit(`${Cypress.config('baseUrl')}/resize-by-content`); - cy.get('h2').should('contain', 'Example 31: Columns Resize by Content'); - }); + describe('Main Tests', () => { + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/resize-by-content`); + cy.get('h2').should('contain', 'Example 31: Columns Resize by Content'); + }); - it('should have cell that fit the text content', () => { - cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.gt', 75); - cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.gt', 67); - cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.gt', 59); - cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.gt', 102); - cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.gt', 89); - cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.gt', 72); - cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.gt', 67); - cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.gt', 72); - cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 179); - cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.gt', 94); - cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); - }); + it('should have cell that fit the text content', () => { + cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.gt', 75); + cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.gt', 67); + cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.gt', 59); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.gt', 102); + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.gt', 89); + cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.gt', 72); + cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.gt', 67); + cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.gt', 72); + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 179); + cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.gt', 94); + cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); + }); - it('should make the grid readonly and expect to fit the text by content and expect column width to be the same as earlier', () => { - cy.get('[data-test="toggle-readonly-btn"]').click(); - - cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.gt', 75); - cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.gt', 67); - cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.gt', 59); - cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.gt', 102); - cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.gt', 89); - cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.gt', 72); - cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.gt', 67); - cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.gt', 72); - cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 179); - cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.gt', 94); - cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); - }); + it('should make the grid readonly and expect to fit the text by content and expect column width to be the same as earlier', () => { + cy.get('[data-test="toggle-readonly-btn"]').click(); + + cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.gt', 75); + cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.gt', 67); + cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.gt', 59); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.gt', 102); + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.gt', 89); + cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.gt', 72); + cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.gt', 67); + cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.gt', 72); + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 179); + cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.gt', 94); + cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); + }); - it('should click on (default resize "autosizeColumns") and expect column to be much thinner and fit all its column within the grid container', () => { - cy.get('[data-test="autosize-columns-btn"]').click(); - - cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.lt', 75); - cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.lt', 95); - cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.lt', 70); - cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.lt', 100); - cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.lt', 100); - cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.lt', 85); - cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.lt', 70); - cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.lt', 85); - cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.lt', 120); - cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.lt', 100); - cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); - }); + it('should click on (default resize "autosizeColumns") and expect column to be much thinner and fit all its column within the grid container', () => { + cy.get('[data-test="autosize-columns-btn"]').click(); + + cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.lt', 75); + cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.lt', 95); + cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.lt', 70); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.lt', 100); + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.lt', 100); + cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.lt', 85); + cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.lt', 70); + cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.lt', 85); + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.lt', 120); + cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.lt', 100); + cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); + }); - it('should double-click on the "Complexity" column resize handle and expect the column to become wider and show all text', () => { - cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.lt', 80); + it('should double-click on the "Complexity" column resize handle and expect the column to become wider and show all text', () => { + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.lt', 80); - cy.get('.slick-header-column:nth-child(6) .slick-resizable-handle') - .dblclick(); + cy.get('.slick-header-column:nth-child(6) .slick-resizable-handle') + .dblclick(); - cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.gt', 95); - }); + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.gt', 95); + }); - it('should open the "Product" header menu and click on "Resize by Content" and expect the column to become wider and show all text', () => { - cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.lt', 120); + it('should open the "Product" header menu and click on "Resize by Content" and expect the column to become wider and show all text', () => { + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.lt', 120); - cy.get('#grid31') - .find('.slick-header-column:nth-child(10)') - .trigger('mouseover') - .children('.slick-header-menu-button') - .invoke('show') - .click(); + cy.get('#grid31') + .find('.slick-header-column:nth-child(10)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); - cy.get('.slick-header-menu .slick-menu-command-list') - .should('be.visible') - .children('.slick-menu-item:nth-of-type(1)') - .children('.slick-menu-content') - .should('contain', 'Resize by Content') - .click(); + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(1)') + .children('.slick-menu-content') + .should('contain', 'Resize by Content') + .click(); - cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 120); - }); + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 120); + }); - it('should change row selection across multiple pages, first page should have 2 selected', () => { - cy.get('[data-test="set-dynamic-rows-btn"]').click(); + it('should change row selection across multiple pages, first page should have 2 selected', () => { + cy.get('[data-test="set-dynamic-rows-btn"]').click(); - // Row index 3, 4 and 11 (last one will be on 2nd page) - cy.get('input[type="checkbox"]:checked').should('have.length', 2); // 2x in current page and 1x in next page - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); - }); + // Row index 3, 4 and 11 (last one will be on 2nd page) + cy.get('input[type="checkbox"]:checked').should('have.length', 2); // 2x in current page and 1x in next page + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); + }); - it('should go to next page and expect 1 row selected in that second page', () => { - cy.get('.icon-seek-next').click(); + it('should go to next page and expect 1 row selected in that second page', () => { + cy.get('.icon-seek-next').click(); - cy.get('input[type="checkbox"]:checked').should('have.length', 1); // only 1x row in page 2 - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); - }); + cy.get('input[type="checkbox"]:checked').should('have.length', 1); // only 1x row in page 2 + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); + }); - it('should click on "Select All" checkbox and expect all rows selected in current page', () => { - const expectedRowIds = [11, 3, 4]; + it('should click on "Select All" checkbox and expect all rows selected in current page', () => { + const expectedRowIds = [11, 3, 4]; - // go back to 1st page - cy.get('.icon-seek-prev') - .click(); + // go back to 1st page + cy.get('.icon-seek-prev') + .click(); - cy.get('#filter-checkbox-selectall-container input[type=checkbox]') - .click({ force: true }); + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .click({ force: true }); - cy.window().then((win) => { - expect(win.console.log).to.have.callCount(3); - expect(win.console.log).to.be.calledWith('Selected Ids:', expectedRowIds); + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(3); + expect(win.console.log).to.be.calledWith('Selected Ids:', expectedRowIds); + }); }); - }); - it('should go to the next 2 pages and expect all rows selected in each page', () => { - cy.get('.icon-seek-next') - .click(); + it('should go to the next 2 pages and expect all rows selected in each page', () => { + cy.get('.icon-seek-next') + .click(); - cy.get('.slick-cell-checkboxsel input:checked') - .should('have.length', 10); + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 10); - cy.get('.icon-seek-next') - .click(); + cy.get('.icon-seek-next') + .click(); - cy.get('.slick-cell-checkboxsel input:checked') - .should('have.length', 10); - }); + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 10); + }); - it('should uncheck 1 row and expect current and next page to have "Select All" uncheck', () => { - cy.get('.slick-row:nth(0) .slick-cell:nth(0) input[type=checkbox]') - .click({ force: true }); + it('should uncheck 1 row and expect current and next page to have "Select All" uncheck', () => { + cy.get('.slick-row:nth(0) .slick-cell:nth(0) input[type=checkbox]') + .click({ force: true }); - cy.get('#filter-checkbox-selectall-container input[type=checkbox]') - .should('not.be.checked', true); + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('not.be.checked', true); - cy.get('.icon-seek-next') - .click(); + cy.get('.icon-seek-next') + .click(); - cy.get('#filter-checkbox-selectall-container input[type=checkbox]') - .should('not.be.checked', true); - }); + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('not.be.checked', true); + }); - it('should go back to previous page, select the row that was unchecked and expect "Select All" to be selected again', () => { - cy.get('.icon-seek-prev') - .click(); + it('should go back to previous page, select the row that was unchecked and expect "Select All" to be selected again', () => { + cy.get('.icon-seek-prev') + .click(); - cy.get('.slick-row:nth(0) .slick-cell:nth(0) input[type=checkbox]') - .click({ force: true }); + cy.get('.slick-row:nth(0) .slick-cell:nth(0) input[type=checkbox]') + .click({ force: true }); - cy.get('#filter-checkbox-selectall-container input[type=checkbox]') - .should('be.checked', true); + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('be.checked', true); - cy.get('.icon-seek-next') - .click(); + cy.get('.icon-seek-next') + .click(); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('be.checked', true); + }); - cy.get('#filter-checkbox-selectall-container input[type=checkbox]') - .should('be.checked', true); + it('should Unselect All and expect all pages to no longer have any row selected', () => { + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .click({ force: true }); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 0); + + cy.get('.icon-seek-prev') + .click(); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 0); + + cy.get('.icon-seek-prev') + .click(); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 0); + }); }); - it('should Unselect All and expect all pages to no longer have any row selected', () => { - cy.get('#filter-checkbox-selectall-container input[type=checkbox]') - .click({ force: true }); + describe('Filter Predicate on "Title" column that act similarly to an SQL LIKE matcher', () => { + it('should return 4 rows using "%10" (ends with 10)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('%10'); - cy.get('.slick-cell-checkboxsel input:checked') - .should('have.length', 0); + cy.get('[data-test="total-items"]') + .should('contain', 4); - cy.get('.icon-seek-prev') - .click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 10'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 110'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 210'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 310'); + }); - cy.get('.slick-cell-checkboxsel input:checked') - .should('have.length', 0); + it('should return 4 rows using "%ask%20" (contains "ask" + ends with 20)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('%ask%20'); - cy.get('.icon-seek-prev') - .click(); + cy.get('[data-test="total-items"]') + .should('contain', 4); - cy.get('.slick-cell-checkboxsel input:checked') - .should('have.length', 0); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 20'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 120'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 220'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 320'); + }); + + it('should return all 400 rows using "%ask%" (contains "ask")', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('%ask%'); + + cy.get('[data-test="total-items"]') + .should('contain', 400); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 3'); + }); + + it('should return 4 rows using "Ta%30" (starts with "Ta" + ends with 30)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('Ta%30'); + + cy.get('[data-test="total-items"]') + .should('contain', 4); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 30'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 130'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 230'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 330'); + }); + + it('should return 14 rows using "Ta%30%" (starts with "Ta" + ends with 30)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('Ta%30%'); + + cy.get('[data-test="total-items"]') + .should('contain', 4); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 30'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 130'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 230'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 300'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 301'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 302'); + }); + + it('should return all 400 rows using "Ta%" (starts with "Ta")', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('Ta%'); + + cy.get('[data-test="total-items"]') + .should('contain', 400); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 3'); + }); + + it('should return 14 rows using "25" (contains 25)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('25'); + + cy.get('[data-test="total-items"]') + .should('contain', 14); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 25'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 125'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 225'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 250'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 251'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 252'); + }); + + it('should not return any row when filtering Title with "%%"', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('%%'); + + cy.get('[data-test="total-items"]') + .should('contain', 0); + }); + + it('return some rows (not all 400) filtering Title as "%ask%" AND a Duration ">50" to test few filters still working', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('%ask%'); + + cy.get('[data-test="total-items"]') + .should('contain', 400); + + cy.get('.search-filter.filter-duration') + .clear() + .type('>50'); + + cy.get('[data-test="total-items"]') + .should('not.contain', 0); + + cy.get('[data-test="total-items"]') + .should('not.contain', 400); + }); }); });