diff --git a/docs/column-functionalities/filters/compound-filters.md b/docs/column-functionalities/filters/compound-filters.md
index 15f9bce35..8972c0ff3 100644
--- a/docs/column-functionalities/filters/compound-filters.md
+++ b/docs/column-functionalities/filters/compound-filters.md
@@ -9,6 +9,7 @@
- [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)
+- [Filter Shortcuts](input-filter.md#filter-shortcuts)
### 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/input-filter.md b/docs/column-functionalities/filters/input-filter.md
index 614e9f10e..4769d6bb3 100644
--- a/docs/column-functionalities/filters/input-filter.md
+++ b/docs/column-functionalities/filters/input-filter.md
@@ -8,6 +8,7 @@
- [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)
+- [Filter Shortcuts](#filter-shortcuts)
### Description
Input text filter is the default filter that will be used when the user ommits the `filter.model`.
@@ -215,3 +216,38 @@ this.columnDefinitions = [
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)
+
+### Filter Shortcuts
+
+User can declare some Filter Shortcuts, that will be added to the Header Menu of the Column it was assigned. These shortcuts are simply a list of filter search values (e.g. Filter the Blank/Non-Blanks Values), the end user can type the same search values themselves but the shortcuts are simply meant to be quicker without having to know what to type (e.g. Filter Current Year).
+
+ The shortcuts can be declared via an array that must include at least a `title` (or `titleKey`) a `searchTerms` array and lastly an optional `operator` can also be provided. The available properties of these shortcut is a merge of Header Menu Item interface (except `command` and `action` which are reserved and assigned internally) and of course the 3 properties mentioned above. The declaration is very similar to how we use it when declaring Grid Presets as shown below
+
+```ts
+this.columnDefinitions = [
+ {
+ id: 'country', name: 'Country', field: 'country',
+ filter: {
+ model: Filters.inputText,
+ filterShortcuts: [
+ { title: 'Blank Values', searchTerms: ['A'], operator: '<', iconCssClass: 'mdi mdi-filter-minus-outline', },
+ { title: 'Non-Blank Values', searchTerms: ['A'], operator: '>', iconCssClass: 'mdi mdi-filter-plus-outline', },
+ ]
+ },
+ },
+ {
+ id: 'finish', name: 'Finish', field: 'finish',
+ filter: {
+ model: Filters.dateRange,
+ filterShortcuts: [
+ {
+ // using Locale translations & Tempo to calculate next 30 days
+ titleKey: 'NEXT_30_DAYS',
+ iconCssClass: 'mdi mdi-calendar',
+ searchTerms: [tempoFormat(new Date(), 'YYYY-MM-DD'), tempoFormat(addDay(new Date(), 30), 'YYYY-MM-DD')],
+ },
+ ]
+ },
+ },
+];
+```
\ No newline at end of file
diff --git a/docs/column-functionalities/filters/range-filters.md b/docs/column-functionalities/filters/range-filters.md
index 73d86fce1..aadb4adf0 100644
--- a/docs/column-functionalities/filters/range-filters.md
+++ b/docs/column-functionalities/filters/range-filters.md
@@ -6,6 +6,7 @@
- [Using a Date Range](#using-a-date-range-filter)
- [Update Filters Dynamically](input-filter.md#update-filters-dynamically)
- [Custom Filter Predicate](input-filter.md#custom-filter-predicate)
+- [Filter Shortcuts](input-filter.md#filter-shortcuts)
### 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
index 8e20b7939..5bfe16569 100644
--- a/docs/column-functionalities/filters/select-filter.md
+++ b/docs/column-functionalities/filters/select-filter.md
@@ -21,6 +21,7 @@
- [Query against a different field](#query-against-another-field-property)
- [Update Filters Dynamically](input-filter.md#update-filters-dynamically)
- [Custom Filter Predicate](input-filter.md#custom-filter-predicate)
+- [Filter Shortcuts](input-filter.md#filter-shortcuts)
### Demo
[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example10) / [Demo Component](https://github.com/ghiscoding/slickgrid-universal/blob/master/examples/webpack-demo-vanilla-bundle/src/examples/example10.ts)
diff --git a/examples/vite-demo-vanilla-bundle/public/i18n/en.json b/examples/vite-demo-vanilla-bundle/public/i18n/en.json
index 3d50e5b88..9294844a0 100644
--- a/examples/vite-demo-vanilla-bundle/public/i18n/en.json
+++ b/examples/vite-demo-vanilla-bundle/public/i18n/en.json
@@ -25,6 +25,7 @@
"EXPORT_TO_EXCEL": "Export to Excel",
"EXPORT_TO_TAB_DELIMITED": "Export in Text format (Tab delimited)",
"EXPORT_TO_TEXT_FORMAT": "Export in Text format",
+ "FILTER_SHORTCUTS": "Filter Shortcuts",
"FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} of {{totalItems}} items",
"FORCE_FIT_COLUMNS": "Force fit columns",
"FREEZE_COLUMNS": "Freeze Columns",
@@ -93,6 +94,7 @@
"MEDIUM": "Medium",
"MALE": "Male",
"NAME": "Name",
+ "NEXT_20_DAYS": "Next 20 days",
"NONE": "None",
"PERCENT_COMPLETE": "% Complete",
"PRIORITY": "Priority",
diff --git a/examples/vite-demo-vanilla-bundle/public/i18n/fr.json b/examples/vite-demo-vanilla-bundle/public/i18n/fr.json
index 800ddf788..ec11e2a3a 100644
--- a/examples/vite-demo-vanilla-bundle/public/i18n/fr.json
+++ b/examples/vite-demo-vanilla-bundle/public/i18n/fr.json
@@ -25,6 +25,7 @@
"EXPORT_TO_EXCEL": "Exporter vers Excel",
"EXPORT_TO_TAB_DELIMITED": "Exporter en format texte (délimité par tabulation)",
"EXPORT_TO_TEXT_FORMAT": "Exporter en format texte",
+ "FILTER_SHORTCUTS": "Raccourcis de filtre",
"FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} de {{totalItems}} éléments",
"FORCE_FIT_COLUMNS": "Ajustement forcé des colonnes",
"FREEZE_COLUMNS": "Geler les colonnes",
@@ -93,6 +94,7 @@
"MEDIUM": "Moyen",
"MALE": "Masculin",
"NAME": "Nom",
+ "NEXT_20_DAYS": "20 prochain jours",
"NONE": "Aucun",
"PERCENT_COMPLETE": "% Achevée",
"PRIORITY": "Priorité",
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example10.html b/examples/vite-demo-vanilla-bundle/src/examples/example10.html
index 940cf169e..c83d83980 100644
--- a/examples/vite-demo-vanilla-bundle/src/examples/example10.html
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example10.html
@@ -19,8 +19,8 @@
(*) NO DATA SHOWN
- just change any of Filters/Sorting/Pages and look at the "GraphQL Query" changing.
Also note that the column Name has a filter with a custom %% operator that behaves like an SQL LIKE operator supporting % wildcards.
- Depending on your configuration, your GraphQL Server might already support regex querying (e.g. Hasura [_regex](https://hasura.io/docs/latest/queries/postgres/filters/text-search-operators/#_regex))
- or you could add your own implementation (e.g. see this SO: https://stackoverflow.com/a/37981802/1212166).
+ Depending on your configuration, your GraphQL Server might already support regex querying (e.g. Hasura _regex)
+ or you could add your own implementation (e.g. see this SO).
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example10.ts b/examples/vite-demo-vanilla-bundle/src/examples/example10.ts
index 7d8eb5548..3aefef2e7 100644
--- a/examples/vite-demo-vanilla-bundle/src/examples/example10.ts
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example10.ts
@@ -13,7 +13,7 @@ import {
import { BindingEventService } from '@slickgrid-universal/binding';
import { GraphqlService, type GraphqlPaginatedResult, type GraphqlServiceApi, type GraphqlServiceOption, } from '@slickgrid-universal/graphql';
import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
-import { addDay, format } from '@formkit/tempo';
+import { addDay, format as tempoFormat } from '@formkit/tempo';
import { type MultipleSelectOption } from 'multiple-select-vanilla';
import { ExampleGridOptions } from './example-grid-options';
@@ -135,12 +135,19 @@ export default class Example10 {
filterable: true,
filter: {
model: Filters.dateRange,
+ filterShortcuts: [
+ {
+ titleKey: 'NEXT_20_DAYS',
+ iconCssClass: 'mdi mdi-calendar',
+ searchTerms: [tempoFormat(new Date(), 'YYYY-MM-DD'), tempoFormat(addDay(new Date(), 20), 'YYYY-MM-DD')],
+ },
+ ]
}
},
];
- const presetLowestDay = format(addDay(new Date(), -2), 'YYYY-MM-DD');
- const presetHighestDay = format(addDay(new Date(), 20), 'YYYY-MM-DD');
+ const presetLowestDay = tempoFormat(addDay(new Date(), -2), 'YYYY-MM-DD');
+ const presetHighestDay = tempoFormat(addDay(new Date(), 20), 'YYYY-MM-DD');
this.gridOptions = {
enableAutoTooltip: true,
@@ -328,8 +335,8 @@ export default class Example10 {
}
setFiltersDynamically() {
- const presetLowestDay = format(addDay(new Date(), -2), 'YYYY-MM-DD');
- const presetHighestDay = format(addDay(new Date(), 20), 'YYYY-MM-DD');
+ const presetLowestDay = tempoFormat(addDay(new Date(), -2), 'YYYY-MM-DD');
+ const presetHighestDay = tempoFormat(addDay(new Date(), 20), 'YYYY-MM-DD');
// we can Set Filters Dynamically (or different filters) afterward through the FilterService
this.sgb.filterService.updateFilters([
@@ -350,8 +357,8 @@ export default class Example10 {
}
resetToOriginalPresets() {
- const presetLowestDay = format(addDay(new Date(), -2), 'YYYY-MM-DD');
- const presetHighestDay = format(addDay(new Date(), 20), 'YYYY-MM-DD');
+ const presetLowestDay = tempoFormat(addDay(new Date(), -2), 'YYYY-MM-DD');
+ const presetHighestDay = tempoFormat(addDay(new Date(), 20), 'YYYY-MM-DD');
this.sgb?.filterService.updateFilters([
// you can use OperatorType or type them as string, e.g.: operator: 'EQ'
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example11.ts b/examples/vite-demo-vanilla-bundle/src/examples/example11.ts
index 901d6686e..48cb806d9 100644
--- a/examples/vite-demo-vanilla-bundle/src/examples/example11.ts
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example11.ts
@@ -27,6 +27,7 @@ import { BindingEventService } from '@slickgrid-universal/binding';
import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin';
import { ExcelExportService } from '@slickgrid-universal/excel-export';
import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
+import { format as tempoFormat, addDay } from '@formkit/tempo';
import { type MultipleSelectOption } from 'multiple-select-vanilla';
import exampleModal from './example11-modal.html?raw';
@@ -172,7 +173,8 @@ export default class Example11 {
id: 'start', name: 'Start', field: 'start', sortable: true, minWidth: 80,
formatter: Formatters.dateIso,
type: FieldType.date, outputType: FieldType.dateIso,
- filterable: true, filter: { model: Filters.compoundDate },
+ filterable: true,
+ filter: { model: Filters.compoundDate },
editor: { model: Editors.date, massUpdate: true },
},
{
@@ -180,7 +182,14 @@ export default class Example11 {
editor: { model: Editors.date, massUpdate: true, editorOptions: { range: { min: 'today' } } as VanillaCalendarOption },
formatter: Formatters.dateIso,
type: FieldType.date, outputType: FieldType.dateIso,
- filterable: true, filter: { model: Filters.compoundDate },
+ filterable: true,
+ filter: {
+ model: Filters.compoundDate,
+ filterShortcuts: [
+ { title: 'Until Now', searchTerms: [tempoFormat(new Date(), 'YYYY-MM-DD')], operator: '<=', iconCssClass: 'mdi mdi-calendar', },
+ { title: 'In the Future', searchTerms: [tempoFormat(addDay(new Date(), 1), 'YYYY-MM-DD')], operator: '>=', iconCssClass: 'mdi mdi-calendar-clock', },
+ ]
+ },
},
{
id: 'completed', name: 'Completed', field: 'completed', width: 80, minWidth: 80, maxWidth: 100,
@@ -265,7 +274,11 @@ export default class Example11 {
model: Filters.inputText,
type: 'string',
queryField: 'countryOfOrigin.name',
- }
+ filterShortcuts: [
+ { title: 'Blank Values', searchTerms: ['< A'], iconCssClass: 'mdi mdi-filter-minus-outline', },
+ { title: 'Non-Blank Values', searchTerms: ['> A'], iconCssClass: 'mdi mdi-filter-plus-outline', },
+ ]
+ },
},
{
id: 'action', name: 'Action', field: 'action', minWidth: 70, width: 75, maxWidth: 75,
@@ -343,6 +356,7 @@ export default class Example11 {
},
headerMenu: {
hideFreezeColumnsCommand: false,
+ subItemChevronClass: 'mdi mdi-chevron-down mdi-rotate-270',
},
gridMenu: {
hideClearFrozenColumnsCommand: false,
diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts
index 12247e399..a4d10724e 100644
--- a/packages/common/src/constants.ts
+++ b/packages/common/src/constants.ts
@@ -35,6 +35,7 @@ export class Constants {
TEXT_EXPORT_TO_EXCEL: 'Export to Excel',
TEXT_EXPORT_TO_TAB_DELIMITED: 'Export in Text format (Tab delimited)',
TEXT_FORCE_FIT_COLUMNS: 'Force fit columns',
+ TEXT_FILTER_SHORTCUTS: 'Filter Shortcuts',
TEXT_FREEZE_COLUMNS: 'Freeze Columns',
TEXT_GREATER_THAN: 'Greater than',
TEXT_GREATER_THAN_OR_EQUAL_TO: 'Greater than or equal to',
diff --git a/packages/common/src/extensions/__tests__/slickHeaderMenu.spec.ts b/packages/common/src/extensions/__tests__/slickHeaderMenu.spec.ts
index 5821599c4..4dbacf3f5 100644
--- a/packages/common/src/extensions/__tests__/slickHeaderMenu.spec.ts
+++ b/packages/common/src/extensions/__tests__/slickHeaderMenu.spec.ts
@@ -1,6 +1,6 @@
import { BasePubSubService } from '@slickgrid-universal/event-pub-sub';
-import type { Column, ColumnSort, ElementPosition, GridOption, HeaderButtonsOrMenu, HeaderMenuItems, MenuCommandItem } from '../../interfaces/index';
+import type { Column, ColumnSort, ElementPosition, Filter, GridOption, HeaderButtonsOrMenu, HeaderMenuItems, MenuCommandItem } from '../../interfaces/index';
import { SlickHeaderMenu } from '../slickHeaderMenu';
import { BackendUtilityService, FilterService, SharedService, SortService } from '../../services';
import { ExtensionUtility } from '../../extensions/extensionUtility';
@@ -73,6 +73,7 @@ const dataViewStub = {
const filterServiceStub = {
clearFilterByColumnId: jest.fn(),
clearFilters: jest.fn(),
+ getFiltersMetadata: jest.fn(),
} as unknown as FilterService;
const pubSubServiceStub = {
@@ -827,8 +828,8 @@ describe('HeaderMenu Plugin', () => {
const commandDivElm = gridContainerDiv.querySelector('[data-command="freeze-columns"]') as HTMLDivElement;
expect((originalColumnDefinitions[1] as any).header!.menu!.commandItems!).toEqual([
- { iconCssClass: 'mdi mdi-pin-outline', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
- { divider: true, command: '', positionOrder: 49 },
+ { iconCssClass: 'mdi mdi-pin-outline', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 45 },
+ { divider: true, command: '', positionOrder: 48 },
]);
expect(commandDivElm).toBeFalsy();
});
@@ -848,7 +849,7 @@ describe('HeaderMenu Plugin', () => {
headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
const clearFilterSpy = jest.spyOn(filterServiceStub, 'clearFilterByColumnId');
- const headerMenuExpected = [{ iconCssClass: 'mdi mdi-filter-remove-outline', title: 'Remove Filter', titleKey: 'REMOVE_FILTER', command: 'clear-filter', positionOrder: 53 }];
+ const headerMenuExpected = [{ iconCssClass: 'mdi mdi-filter-remove-outline', title: 'Remove Filter', titleKey: 'REMOVE_FILTER', command: 'clear-filter', positionOrder: 57 }];
const commandDivElm = gridContainerDiv.querySelector('[data-command="clear-filter"]') as HTMLDivElement;
const commandIconElm = commandDivElm.querySelector('.slick-menu-icon') as HTMLDivElement;
const commandLabelElm = commandDivElm.querySelector('.slick-menu-content') as HTMLDivElement;
@@ -884,9 +885,9 @@ describe('HeaderMenu Plugin', () => {
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');
const headerMenuExpected = [
- { iconCssClass: 'mdi mdi-arrow-expand-horizontal', title: 'Resize by Content', titleKey: 'COLUMN_RESIZE_BY_CONTENT', command: 'column-resize-by-content', positionOrder: 48 },
- { divider: true, command: '', positionOrder: 49 },
- { iconCssClass: 'mdi mdi-close', title: 'Hide Column', titleKey: 'HIDE_COLUMN', command: 'hide-column', positionOrder: 55 }
+ { iconCssClass: 'mdi mdi-arrow-expand-horizontal', title: 'Resize by Content', titleKey: 'COLUMN_RESIZE_BY_CONTENT', command: 'column-resize-by-content', positionOrder: 47 },
+ { divider: true, command: '', positionOrder: 48 },
+ { iconCssClass: 'mdi mdi-close', title: 'Hide Column', titleKey: 'HIDE_COLUMN', command: 'hide-column', positionOrder: 59 }
];
const commandDivElm = gridContainerDiv.querySelector('[data-command="column-resize-by-content"]') as HTMLDivElement;
const commandIconElm = commandDivElm.querySelector('.slick-menu-icon') as HTMLDivElement;
@@ -900,6 +901,61 @@ describe('HeaderMenu Plugin', () => {
expect(pubSubSpy).toHaveBeenCalledWith('onHeaderMenuColumnResizeByContent', { columnId: 'field2' });
});
+ it('should populate Filter Shortcuts list with shortcuts as sub-menus when a shortcu list is provided and we should also expect the command to execute necessary callback', () => {
+ columnsMock[0].filter = {
+ filterShortcuts: [
+ { title: 'Blank Values', searchTerms: ['A'], operator: '<', iconCssClass: 'mdi mdi-filter-minus-outline', },
+ { title: 'Non-Blank Values', searchTerms: ['A'], operator: '>', iconCssClass: 'mdi mdi-filter-plus-outline', },
+ ]
+ };
+ jest.spyOn(SharedService.prototype.slickGrid, 'getColumns').mockReturnValueOnce(columnsMock);
+ jest.spyOn(SharedService.prototype.slickGrid, 'getColumnIndex').mockReturnValue(0);
+ const setValueMock = jest.fn();
+ const filterMock = { columnDef: columnsMock[0], setValues: setValueMock } as unknown as Filter;
+ jest.spyOn(filterServiceStub, 'getFiltersMetadata').mockReturnValueOnce([filterMock]);
+
+ // calling `onBeforeSetColumns` 2x times shouldn't duplicate any column menus
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData as any, gridStub);
+ gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData as any, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData as any, gridStub);
+ gridStub.onHeaderCellRendered.notify({ column: columnsMock[0], node: headerDiv, grid: gridStub }, eventData as any, gridStub);
+ const headerButtonElm = headerDiv.querySelector('.slick-header-menu-button') as HTMLDivElement;
+ headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
+
+ const headerMenuExpected = [
+ { command: 'freeze-columns', iconCssClass: 'mdi mdi-pin-outline', positionOrder: 45, title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', },
+ { command: 'show-negative-numbers', cssClass: 'mdi mdi-lightbulb-on', tooltip: 'Highlight negative numbers.', },
+ { command: 'column-resize-by-content', iconCssClass: 'mdi mdi-arrow-expand-horizontal', positionOrder: 47, title: 'Resize by Content', titleKey: 'COLUMN_RESIZE_BY_CONTENT', },
+ { command: '', divider: true, positionOrder: 48, },
+ {
+ command: 'filter-shortcuts-root-menu',
+ commandItems: [
+ { command: 'blank-values', action: expect.any(Function), iconCssClass: 'mdi mdi-filter-minus-outline', operator: '<', searchTerms: ['A'], title: 'Blank Values', },
+ { command: 'non-blank-values', action: expect.any(Function), iconCssClass: 'mdi mdi-filter-plus-outline', operator: '>', searchTerms: ['A'], title: 'Non-Blank Values', },
+ ],
+ iconCssClass: 'mdi mdi-filter-outline',
+ positionOrder: 55,
+ title: 'Filter Shortcuts',
+ titleKey: 'FILTER_SHORTCUTS',
+ },
+ { command: '', divider: true, positionOrder: 56 },
+ { command: 'hide-column', iconCssClass: 'mdi mdi-close', positionOrder: 59, title: 'Hide Column', titleKey: 'HIDE_COLUMN', },
+ ];
+ const shortcutSubMenuElm = gridContainerDiv.querySelector('[data-command="filter-shortcuts-root-menu"]') as HTMLDivElement;
+ shortcutSubMenuElm!.dispatchEvent(new Event('mouseover'));
+ const subCommandShortcut1 = document.body.querySelector('.slick-header-menu.slick-menu-level-1') as HTMLDivElement;
+ const blankValueCommandElm = subCommandShortcut1.querySelector('[data-command="blank-values"]') as HTMLDivElement;
+ const commandIconElm = subCommandShortcut1.querySelector('.slick-menu-icon') as HTMLDivElement;
+ const commandLabelElm = subCommandShortcut1.querySelector('.slick-menu-content') as HTMLDivElement;
+ expect(columnsMock[0].header!.menu!.commandItems!).toEqual(headerMenuExpected);
+ expect(commandIconElm.classList.contains('mdi-filter-minus-outline')).toBeTruthy();
+ expect(commandLabelElm.textContent).toBe('Blank Values');
+
+ const clickEvent = new Event('click');
+ blankValueCommandElm.dispatchEvent(clickEvent);
+ expect(setValueMock).toHaveBeenCalledWith(['A'], '<', true);
+ });
+
it('should expect only the "hide-column" command in the menu when "enableSorting" and "hideSortCommands" are set and also expect the command to execute necessary callback', () => {
jest.spyOn(SharedService.prototype.slickGrid, 'getColumnIndex').mockReturnValue(1);
jest.spyOn(SharedService.prototype.slickGrid, 'getColumns').mockReturnValue(columnsMock);
@@ -918,9 +974,9 @@ describe('HeaderMenu Plugin', () => {
const autosizeSpy = jest.spyOn(gridStub, 'autosizeColumns');
const headerMenuExpected = [
- { iconCssClass: 'mdi mdi-pin-outline', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
- { divider: true, command: '', positionOrder: 49 },
- { iconCssClass: 'mdi mdi-close', title: 'Hide Column', titleKey: 'HIDE_COLUMN', command: 'hide-column', positionOrder: 55 }
+ { iconCssClass: 'mdi mdi-pin-outline', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 45 },
+ { divider: true, command: '', positionOrder: 48 },
+ { iconCssClass: 'mdi mdi-close', title: 'Hide Column', titleKey: 'HIDE_COLUMN', command: 'hide-column', positionOrder: 59 }
];
const commandDivElm = gridContainerDiv.querySelector('[data-command="hide-column"]') as HTMLDivElement;
const commandIconElm = commandDivElm.querySelector('.slick-menu-icon') as HTMLDivElement;
@@ -947,7 +1003,7 @@ describe('HeaderMenu Plugin', () => {
headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
const clearFilterSpy = jest.spyOn(filterServiceStub, 'clearFilterByColumnId');
- const headerMenuExpected = [{ iconCssClass: 'mdi mdi-filter-remove-outline', title: 'Remove Filter', titleKey: 'REMOVE_FILTER', command: 'clear-filter', positionOrder: 53 }];
+ const headerMenuExpected = [{ iconCssClass: 'mdi mdi-filter-remove-outline', title: 'Remove Filter', titleKey: 'REMOVE_FILTER', command: 'clear-filter', positionOrder: 57 }];
const commandDivElm = gridContainerDiv.querySelector('[data-command="clear-filter"]') as HTMLDivElement;
const commandIconElm = commandDivElm.querySelector('.slick-menu-icon') as HTMLDivElement;
const commandLabelElm = commandDivElm.querySelector('.slick-menu-content') as HTMLDivElement;
@@ -981,7 +1037,7 @@ describe('HeaderMenu Plugin', () => {
{ iconCssClass: 'mdi mdi-sort-ascending', title: 'Sort Ascending', titleKey: 'SORT_ASCENDING', command: 'sort-asc', positionOrder: 50 },
{ iconCssClass: 'mdi mdi-sort-descending', title: 'Sort Descending', titleKey: 'SORT_DESCENDING', command: 'sort-desc', positionOrder: 51 },
{ divider: true, command: '', positionOrder: 52 },
- { iconCssClass: 'mdi mdi-sort-variant-off', title: 'Remove Sort', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 54 },
+ { iconCssClass: 'mdi mdi-sort-variant-off', title: 'Remove Sort', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 58 },
]);
expect(commandIconElm.classList.contains('mdi-sort-variant-off')).toBeTruthy();
expect(commandLabelElm.textContent).toBe('Remove Sort');
@@ -992,7 +1048,7 @@ describe('HeaderMenu Plugin', () => {
{ iconCssClass: 'mdi mdi-sort-ascending', title: 'Trier par ordre croissant', titleKey: 'SORT_ASCENDING', command: 'sort-asc', positionOrder: 50 },
{ iconCssClass: 'mdi mdi-sort-descending', title: 'Trier par ordre décroissant', titleKey: 'SORT_DESCENDING', command: 'sort-desc', positionOrder: 51 },
{ divider: true, command: '', positionOrder: 52 },
- { iconCssClass: 'mdi mdi-sort-variant-off', title: 'Supprimer le tri', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 54 },
+ { iconCssClass: 'mdi mdi-sort-variant-off', title: 'Supprimer le tri', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 58 },
]);
const clickEvent = new Event('click');
@@ -1019,8 +1075,8 @@ describe('HeaderMenu Plugin', () => {
const commandIconElm = commandDivElm.querySelector('.slick-menu-icon') as HTMLDivElement;
const commandLabelElm = commandDivElm.querySelector('.slick-menu-content') as HTMLDivElement;
expect(columnsMock[1].header!.menu!.commandItems!).toEqual([
- { iconCssClass: 'mdi mdi-pin-outline', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
- { divider: true, command: '', positionOrder: 49 },
+ { iconCssClass: 'mdi mdi-pin-outline', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 45 },
+ { divider: true, command: '', positionOrder: 48 },
]);
expect(commandIconElm.classList.contains('mdi-pin-outline')).toBeTruthy();
expect(commandLabelElm.textContent).toBe('Freeze Columns');
@@ -1028,8 +1084,8 @@ describe('HeaderMenu Plugin', () => {
translateService.use('fr');
plugin.translateHeaderMenu();
expect(columnsMock[1].header!.menu!.commandItems!).toEqual([
- { iconCssClass: 'mdi mdi-pin-outline', title: 'Geler les colonnes', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
- { divider: true, command: '', positionOrder: 49 },
+ { iconCssClass: 'mdi mdi-pin-outline', title: 'Geler les colonnes', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 45 },
+ { divider: true, command: '', positionOrder: 48 },
]);
commandDivElm.dispatchEvent(new Event('click')); // execute command
@@ -1052,8 +1108,8 @@ describe('HeaderMenu Plugin', () => {
const commandDivElm = gridContainerDiv.querySelector('[data-command="freeze-columns"]') as HTMLDivElement;
expect(columnsMock[2].header!.menu!.commandItems!).toEqual([
- { iconCssClass: 'mdi mdi-pin-outline', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
- { divider: true, command: '', positionOrder: 49 },
+ { iconCssClass: 'mdi mdi-pin-outline', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 45 },
+ { divider: true, command: '', positionOrder: 48 },
]);
commandDivElm.dispatchEvent(new Event('click')); // execute command
@@ -1080,8 +1136,8 @@ describe('HeaderMenu Plugin', () => {
const commandDivElm = gridContainerDiv.querySelector('[data-command="freeze-columns"]') as HTMLDivElement;
expect((originalColumnDefinitions[1] as any).header!.menu!.commandItems!).toEqual([
- { iconCssClass: 'mdi mdi-pin-outline', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 47 },
- { divider: true, command: '', positionOrder: 49 },
+ { iconCssClass: 'mdi mdi-pin-outline', title: 'Freeze Columns', titleKey: 'FREEZE_COLUMNS', command: 'freeze-columns', positionOrder: 45 },
+ { divider: true, command: '', positionOrder: 48 },
]);
commandDivElm.dispatchEvent(new Event('click')); // execute command
@@ -1110,7 +1166,7 @@ describe('HeaderMenu Plugin', () => {
{ iconCssClass: 'mdi mdi-sort-ascending', title: 'Sort Ascending', titleKey: 'SORT_ASCENDING', command: 'sort-asc', positionOrder: 50 },
{ iconCssClass: 'mdi mdi-sort-descending', title: 'Sort Descending', titleKey: 'SORT_DESCENDING', command: 'sort-desc', positionOrder: 51 },
{ divider: true, command: '', positionOrder: 52 },
- { iconCssClass: 'mdi mdi-sort-variant-off', title: 'Remove Sort', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 54 },
+ { iconCssClass: 'mdi mdi-sort-variant-off', title: 'Remove Sort', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 58 },
]);
const clickEvent = new Event('click');
@@ -1142,7 +1198,7 @@ describe('HeaderMenu Plugin', () => {
{ iconCssClass: 'mdi mdi-sort-ascending', title: 'Sort Ascending', titleKey: 'SORT_ASCENDING', command: 'sort-asc', positionOrder: 50 },
{ iconCssClass: 'mdi mdi-sort-descending', title: 'Sort Descending', titleKey: 'SORT_DESCENDING', command: 'sort-desc', positionOrder: 51 },
{ divider: true, command: '', positionOrder: 52 },
- { iconCssClass: 'mdi mdi-sort-variant-off', title: 'Remove Sort', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 54 },
+ { iconCssClass: 'mdi mdi-sort-variant-off', title: 'Remove Sort', titleKey: 'REMOVE_SORT', command: 'clear-sort', positionOrder: 58 },
]);
const clickEvent = new Event('click');
diff --git a/packages/common/src/extensions/slickHeaderMenu.ts b/packages/common/src/extensions/slickHeaderMenu.ts
index 50d68e902..5102ca01a 100644
--- a/packages/common/src/extensions/slickHeaderMenu.ts
+++ b/packages/common/src/extensions/slickHeaderMenu.ts
@@ -1,5 +1,5 @@
import type { BasePubSubService } from '@slickgrid-universal/event-pub-sub';
-import { arrayRemoveItemByIndex, calculateAvailableSpace, createDomElement, getOffsetRelativeToParent, getOffset, classNameToList } from '@slickgrid-universal/utils';
+import { arrayRemoveItemByIndex, calculateAvailableSpace, createDomElement, getOffsetRelativeToParent, getOffset, classNameToList, toKebabCase } from '@slickgrid-universal/utils';
import { EmitterType } from '../enums/index';
import type {
@@ -361,7 +361,7 @@ export class SlickHeaderMenu extends MenuBaseClass {
iconCssClass: headerMenuOptions.iconFreezeColumns || 'mdi mdi-pin-outline',
titleKey: `${translationPrefix}FREEZE_COLUMNS`,
command: 'freeze-columns',
- positionOrder: 47
+ positionOrder: 45
});
}
}
@@ -374,14 +374,14 @@ export class SlickHeaderMenu extends MenuBaseClass {
iconCssClass: headerMenuOptions.iconColumnResizeByContentCommand || 'mdi mdi-arrow-expand-horizontal',
titleKey: `${translationPrefix}COLUMN_RESIZE_BY_CONTENT`,
command: 'column-resize-by-content',
- positionOrder: 48
+ positionOrder: 47
});
}
}
// add a divider (separator) between the top freeze columns commands and the rest of the commands
- if (hasFrozenOrResizeCommand && !columnHeaderMenuItems.some(item => item !== 'divider' && item.positionOrder === 49)) {
- columnHeaderMenuItems.push({ divider: true, command: '', positionOrder: 49 });
+ if (hasFrozenOrResizeCommand && !columnHeaderMenuItems.some(item => item !== 'divider' && item.positionOrder === 48)) {
+ columnHeaderMenuItems.push({ divider: true, command: '', positionOrder: 48 });
}
// Sorting Commands
@@ -413,8 +413,41 @@ export class SlickHeaderMenu extends MenuBaseClass {
iconCssClass: headerMenuOptions.iconClearSortCommand || 'mdi mdi-sort-variant-off',
titleKey: `${translationPrefix}REMOVE_SORT`,
command: 'clear-sort',
- positionOrder: 54
+ positionOrder: 58
+ });
+ }
+ }
+
+ // Filter Shortcuts via sub-menus
+ if (columnDef.filter?.filterShortcuts && !columnHeaderMenuItems.some(item => item !== 'divider' && item?.command === 'filter-shortcuts-root-menu')) {
+ const shortcutSubItems: MenuCommandItem[] = [];
+ columnDef.filter.filterShortcuts.forEach(fs => {
+ // use the Title name as the command key in kebab cas
+ const command = fs.title ? toKebabCase(fs.title) : (fs.titleKey || '').toLowerCase().replaceAll('_', '-');
+
+ shortcutSubItems.push({
+ ...fs,
+ command,
+ action: (_e, args) => {
+ // get associated Column Filter instance and use its `setValues()` method to update the filter with provided `searchTerms`
+ const filterRef = this.filterService.getFiltersMetadata().find(f => f.columnDef.id === args.column.id);
+ filterRef?.setValues(fs.searchTerms, fs.operator, true);
+ }
});
+ });
+
+ const filterShortcutsPositionOrder = headerMenuOptions.filterShortcutsPositionOrder ?? 55;
+ columnHeaderMenuItems.push({
+ iconCssClass: headerMenuOptions.iconFilterShortcutSubMenu || 'mdi mdi-filter-outline',
+ titleKey: `${translationPrefix}FILTER_SHORTCUTS`,
+ command: 'filter-shortcuts-root-menu',
+ positionOrder: filterShortcutsPositionOrder,
+ commandItems: shortcutSubItems
+ });
+
+ // add a divider (separator) between the top freeze columns commands and the rest of the commands
+ if (hasFrozenOrResizeCommand && !columnHeaderMenuItems.some(item => item !== 'divider' && item.positionOrder === filterShortcutsPositionOrder + 1)) {
+ columnHeaderMenuItems.push({ divider: true, command: '', positionOrder: filterShortcutsPositionOrder + 1 });
}
}
@@ -425,7 +458,7 @@ export class SlickHeaderMenu extends MenuBaseClass {
iconCssClass: headerMenuOptions.iconClearFilterCommand || 'mdi mdi-filter-remove-outline',
titleKey: `${translationPrefix}REMOVE_FILTER`,
command: 'clear-filter',
- positionOrder: 53
+ positionOrder: 57
});
}
}
@@ -436,7 +469,7 @@ export class SlickHeaderMenu extends MenuBaseClass {
iconCssClass: headerMenuOptions.iconColumnHideCommand || 'mdi mdi-close',
titleKey: `${translationPrefix}HIDE_COLUMN`,
command: 'hide-column',
- positionOrder: 55
+ positionOrder: 59
});
}
diff --git a/packages/common/src/global-grid-options.ts b/packages/common/src/global-grid-options.ts
index ca8896a5e..3e6695981 100644
--- a/packages/common/src/global-grid-options.ts
+++ b/packages/common/src/global-grid-options.ts
@@ -221,6 +221,7 @@ export const GlobalGridOptions: Partial = {
minWidth: 140,
iconClearFilterCommand: 'mdi mdi-filter-remove-outline',
iconClearSortCommand: 'mdi mdi-sort-variant-off',
+ iconFilterShortcutSubMenu: 'mdi mdi-filter-outline',
iconFreezeColumns: 'mdi mdi-pin-outline',
iconSortAscCommand: 'mdi mdi-sort-ascending',
iconSortDescCommand: 'mdi mdi-sort-descending',
diff --git a/packages/common/src/interfaces/columnFilter.interface.ts b/packages/common/src/interfaces/columnFilter.interface.ts
index 5c6e40b9b..4016c1829 100644
--- a/packages/common/src/interfaces/columnFilter.interface.ts
+++ b/packages/common/src/interfaces/columnFilter.interface.ts
@@ -7,6 +7,7 @@ import type {
Column,
Filter,
FilterConstructor,
+ MenuCommandItem,
OperatorDetail,
SearchColumnFilter,
} from './index';
@@ -101,6 +102,13 @@ export interface ColumnFilter {
*/
filterPredicate?: (dataContext: any, searchFilterArgs: SearchColumnFilter) => boolean;
+ /**
+ * A list of optional filter shortcuts to display in the Header Menu,
+ * these shortcuts will simply prefill the column filter values with the shortcuts defined by the user.
+ * For example: `filterShortcuts: { command: 'blanks', title: 'Blanks', searchTerms: ['< a'] }`
+ */
+ filterShortcuts?: Array & { searchTerms: SearchTerm[]; operator?: OperatorType | OperatorString; }>;
+
/**
* Use "params" to pass any type of arguments to your Custom Filter
* for example, to pass a second collection to a select Filter we can type this:
diff --git a/packages/common/src/interfaces/headerMenuOption.interface.ts b/packages/common/src/interfaces/headerMenuOption.interface.ts
index b403a0ea2..3372b2115 100644
--- a/packages/common/src/interfaces/headerMenuOption.interface.ts
+++ b/packages/common/src/interfaces/headerMenuOption.interface.ts
@@ -11,6 +11,9 @@ export interface HeaderMenuOption {
/** an extra CSS class to add to the menu button */
buttonCssClass?: string;
+ /** position order index of the "Filter Shortcuts" menu */
+ filterShortcutsPositionOrder?: number;
+
/** Defaults to false, which will hide the "Column Resize by Content" command in the Header Menu (Grid Option "enableColumnResizeOnDoubleClick" has to also be enabled) */
hideColumnResizeByContentCommand?: boolean;
@@ -53,6 +56,9 @@ export interface HeaderMenuOption {
/** icon for the "Hide Column" command */
iconColumnHideCommand?: string;
+ /** icon for the "Filter Shortcuts" menu (the shortcuts will be displayed as sub-menus of this parent menu) */
+ iconFilterShortcutSubMenu?: string;
+
/** icon for the "Freeze Columns" command */
iconFreezeColumns?: string;
@@ -84,5 +90,5 @@ export interface HeaderMenuOption {
// Methods
/** Callback method that user can override the default behavior of enabling/disabling an item from the list. */
- menuUsabilityOverride?: (args: { grid: SlickGrid, column: Column, menu: HTMLElement }) => boolean;
+ menuUsabilityOverride?: (args: { grid: SlickGrid, column: Column, menu: HTMLElement; }) => boolean;
}
diff --git a/packages/common/src/interfaces/locale.interface.ts b/packages/common/src/interfaces/locale.interface.ts
index 8d3c01796..b0c389dbc 100644
--- a/packages/common/src/interfaces/locale.interface.ts
+++ b/packages/common/src/interfaces/locale.interface.ts
@@ -95,6 +95,9 @@ export interface Locale {
/** Text "Export to Excel" shown in Grid Menu (when enabled) */
TEXT_EXPORT_TO_EXCEL: string;
+ /** Text "Filter Shortcuts" shown in Header Menu (when shortcuts are provided to a column filter) */
+ TEXT_FILTER_SHORTCUTS?: string;
+
/** Text "Force fit Columns" displayed in the Column Picker & Grid Menu (when enabled) */
TEXT_FORCE_FIT_COLUMNS: string;
diff --git a/packages/common/src/styles/slick-plugins.scss b/packages/common/src/styles/slick-plugins.scss
index 484364148..0d97e8c79 100644
--- a/packages/common/src/styles/slick-plugins.scss
+++ b/packages/common/src/styles/slick-plugins.scss
@@ -371,6 +371,7 @@ li.hidden {
}
.sub-item-chevron {
float: right;
+ min-width: var(--slick-menu-icon-min-width, $slick-menu-icon-min-width);
}
/* Disabled item */
diff --git a/packages/utils/src/__tests__/utils.spec.ts b/packages/utils/src/__tests__/utils.spec.ts
index f0ffd3e41..c55ef328c 100644
--- a/packages/utils/src/__tests__/utils.spec.ts
+++ b/packages/utils/src/__tests__/utils.spec.ts
@@ -8,7 +8,6 @@ import {
deepMerge,
emptyObject,
getFunctionDetails,
- hasData,
isDefined,
isDefinedNumber,
isEmptyObject,
@@ -764,6 +763,7 @@ describe('Service/Utilies', () => {
describe('toKebabCase() method', () => {
const sentence = 'the quick brown fox';
+ const snakeCaseSentence = 'the_quick_brown_fox';
it('should return empty string when input is empty', () => {
const output = toKebabCase('');
@@ -781,6 +781,11 @@ describe('Service/Utilies', () => {
expect(output).toBe('the-quick-brown-fox');
});
+ it('should return a kebab-case string when input is a snake_case word', () => {
+ const output = toKebabCase(sentence);
+ expect(output).toBe('the-quick-brown-fox');
+ });
+
it('should return a kebab-case string when input is a sentence that may include numbers with only following char having the dash', () => {
const output = toKebabCase(sentence + ' 123 ' + ' apples');
expect(output).toBe('the-quick-brown-fox123-apples');
@@ -790,6 +795,7 @@ describe('Service/Utilies', () => {
describe('toSentenceCase() method', () => {
const camelCaseSentence = 'theQuickBrownFox';
const kebabCaseSentence = 'the-quick-brown-fox';
+ const snakeCaseSentence = 'the_quick_brown_fox';
it('should return empty string when input is empty', () => {
const output = toSentenceCase('');
@@ -812,6 +818,11 @@ describe('Service/Utilies', () => {
expect(output).toBe('The quick brown fox');
});
+ it('should return a sentence case string when input is snake_case type', () => {
+ const output = toSentenceCase(snakeCaseSentence);
+ expect(output).toBe('The quick brown fox');
+ });
+
it('should return a sentence case string when input is a sentence that may include numbers and extra spaces', () => {
const output = toSentenceCase(kebabCaseSentence + ' 123 ' + ' apples ');
expect(output).toBe('The quick brown fox 123 apples');
diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts
index 55c125f9e..bd6fbb526 100644
--- a/packages/utils/src/utils.ts
+++ b/packages/utils/src/utils.ts
@@ -314,7 +314,7 @@ export function toCamelCase(inputStr: string): string {
*/
export function toKebabCase(inputStr: string): string {
if (typeof inputStr === 'string') {
- return toCamelCase(inputStr).replace(/([A-Z])/g, '-$1').toLowerCase();
+ return toCamelCase(inputStr).replace(/([A-Z])|([-_])/g, '-$1').toLowerCase();
}
return inputStr;
}
@@ -326,7 +326,7 @@ export function toKebabCase(inputStr: string): string {
*/
export function toSentenceCase(inputStr: string): string {
if (typeof inputStr === 'string') {
- const result = inputStr.replace(/([A-Z])|(-)/g, ' $1').replace(/\s+/g, ' ').trim();
+ const result = inputStr.replace(/([A-Z])|([-_])/g, ' $1').replace(/\s+/g, ' ').trim();
return result.charAt(0).toUpperCase() + result.slice(1);
}
return inputStr;
diff --git a/test/cypress/e2e/example10.cy.ts b/test/cypress/e2e/example10.cy.ts
index 7106e739e..a01cd7614 100644
--- a/test/cypress/e2e/example10.cy.ts
+++ b/test/cypress/e2e/example10.cy.ts
@@ -254,7 +254,7 @@ describe('Example 10 - GraphQL Grid', () => {
cy.get('.slick-header-menu .slick-menu-command-list')
.should('be.visible')
- .children('.slick-menu-item:nth-of-type(6)')
+ .children('.slick-menu-item[data-command=clear-filter]')
.children('.slick-menu-content')
.should('contain', 'Remove Filter')
.click();
@@ -900,4 +900,88 @@ describe('Example 10 - GraphQL Grid', () => {
cy.get('h3').click();
});
});
+
+ describe('Filter Shortcuts', () => {
+ const today = format(new Date(), 'YYYY-MM-DD');
+ const next20Day = format(addDay(new Date(), 20), 'YYYY-MM-DD');
+
+ it('should open header menu of "Finish" again then choose "Filter Shortcuts -> In the Future" and expect date range of the next 20 days', () => {
+ cy.get('.grid10')
+ .find('.slick-header-column:nth-of-type(6)')
+ .trigger('mouseover')
+ .children('.slick-header-menu-button')
+ .invoke('show')
+ .click();
+
+ cy.get('[data-command=filter-shortcuts-root-menu]')
+ .trigger('mouseover');
+
+ cy.get('.slick-header-menu.slick-menu-level-1')
+ .find('[data-command=next-20-days]')
+ .should('contain', 'Next 20 days')
+ .click();
+
+ cy.get('.search-filter.filter-finish input.date-picker')
+ .invoke('val')
+ .should('equal', `${today} — ${next20Day}`);
+
+ // 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 = removeSpaces($span.text()); // remove all white spaces
+ expect(text).to.eq(removeSpaces(`query { users (first:20,offset:0,orderBy:[{field:"name",direction:ASC},
+ {field:"company",direction:DESC}],filterBy:[{field:"gender",operator:EQ,value:"male"},
+ {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"},
+ {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${today}"},
+ {field:"finish",operator:LE,value:"${next20Day}"}],locale:"en",userId:123) {
+ totalCount, nodes { id,name,gender,company,billing{address{street,zip}},finish}}}`));
+ });
+ });
+
+ it('should switch locale to French', () => {
+ cy.get('[data-test=language-button]')
+ .click();
+
+ cy.get('[data-test=selected-locale]')
+ .should('contain', 'fr.json');
+ });
+
+ it('should open header menu of "Finish" again now expect French translations "Filter Shortcuts -> In the Future" and expect date range of the next 20 days', () => {
+ cy.get('.grid10')
+ .find('.slick-header-column:nth-of-type(6)')
+ .trigger('mouseover')
+ .children('.slick-header-menu-button')
+ .invoke('show')
+ .click();
+
+ cy.get('[data-command=filter-shortcuts-root-menu]')
+ .should('contain', 'Raccourcis de filtre')
+ .trigger('mouseover');
+
+ cy.get('.slick-header-menu.slick-menu-level-1')
+ .find('[data-command=next-20-days]')
+ .should('contain', '20 prochain jours')
+ .click();
+
+ cy.get('.search-filter.filter-finish input.date-picker')
+ .invoke('val')
+ .should('equal', `${today} — ${next20Day}`);
+
+ // 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 = removeSpaces($span.text()); // remove all white spaces
+ expect(text).to.eq(removeSpaces(`query { users (first:20,offset:0,orderBy:[{field:"name",direction:ASC},
+ {field:"company",direction:DESC}],filterBy:[{field:"gender",operator:EQ,value:"male"},
+ {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"},
+ {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${today}"},
+ {field:"finish",operator:LE,value:"${next20Day}"}],locale:"fr",userId:123) {
+ totalCount, nodes { id,name,gender,company,billing{address{street,zip}},finish}}}`));
+ });
+ });
+ });
});
\ No newline at end of file
diff --git a/test/cypress/e2e/example11.cy.ts b/test/cypress/e2e/example11.cy.ts
index d20373c39..25bd65f9a 100644
--- a/test/cypress/e2e/example11.cy.ts
+++ b/test/cypress/e2e/example11.cy.ts
@@ -1,3 +1,5 @@
+import { addDay, format } from '@formkit/tempo';
+
import { changeTimezone, zeroPadding } from '../plugins/utilities';
describe('Example 11 - Batch Editing', () => {
@@ -115,6 +117,124 @@ describe('Example 11 - Batch Editing', () => {
expect(Number($span.text())).to.eq(484);
});
});
+
+ it('should open header menu of "Country of Origin" and choose "Filter Shortcuts -> Blanks Values" and expect 31 rows', () => {
+ cy.get('.grid11')
+ .find('.slick-header-column:nth-of-type(10)')
+ .trigger('mouseover')
+ .children('.slick-header-menu-button')
+ .invoke('show')
+ .click();
+
+ cy.get('[data-command=filter-shortcuts-root-menu]')
+ .should('contain', 'Filter Shortcuts')
+ .trigger('mouseover');
+
+ cy.get('.slick-header-menu.slick-menu-level-1')
+ .find('[data-command=blank-values]')
+ .click();
+
+ cy.get('.search-filter.filter-countryOfOrigin')
+ .invoke('val')
+ .should('equal', '< A');
+
+ cy.get('.grid11')
+ .find('.slick-custom-footer')
+ .find('.right-footer .item-count')
+ .should($span => {
+ expect(Number($span.text())).to.eq(31);
+ });
+ });
+
+ it('should open header menu of "Country of Origin" again then choose "Filter Shortcuts -> Non-Blanks Values" and expect 969 rows', () => {
+ cy.get('.grid11')
+ .find('.slick-header-column:nth-of-type(10)')
+ .trigger('mouseover')
+ .children('.slick-header-menu-button')
+ .invoke('show')
+ .click();
+
+ cy.get('[data-command=filter-shortcuts-root-menu]')
+ .should('contain', 'Filter Shortcuts')
+ .trigger('mouseover');
+
+ cy.get('.slick-header-menu.slick-menu-level-1')
+ .find('[data-command=non-blank-values]')
+ .click();
+
+ cy.get('.search-filter.filter-countryOfOrigin')
+ .invoke('val')
+ .should('equal', '> A');
+
+ cy.get('.grid11')
+ .find('.slick-custom-footer')
+ .find('.right-footer .item-count')
+ .should($span => {
+ expect(Number($span.text())).to.eq(969);
+ });
+ });
+
+ it('should open header menu of "Finish" and choose "Filter Shortcuts -> Until Now" and expect below 969 rows', () => {
+ cy.get('.grid11')
+ .find('.slick-header-column:nth-of-type(7)')
+ .trigger('mouseover')
+ .children('.slick-header-menu-button')
+ .invoke('show')
+ .click();
+
+ cy.get('[data-command=filter-shortcuts-root-menu]')
+ .should('contain', 'Filter Shortcuts')
+ .trigger('mouseover');
+
+ cy.get('.slick-header-menu.slick-menu-level-1')
+ .find('[data-command=until-now]')
+ .click();
+
+ cy.get('.search-filter.filter-finish .input-group-prepend.operator select')
+ .contains('<=');
+
+ cy.get('.search-filter.filter-finish input.date-picker')
+ .invoke('val')
+ .should('equal', format(new Date(), 'YYYY-MM-DD'));
+
+ cy.get('.grid11')
+ .find('.slick-custom-footer')
+ .find('.right-footer .item-count')
+ .should($span => {
+ expect(Number($span.text())).to.lt(969);
+ });
+ });
+
+ it('should open header menu of "Finish" again then choose "Filter Shortcuts -> In the Future" and expect below 969 rows', () => {
+ cy.get('.grid11')
+ .find('.slick-header-column:nth-of-type(7)')
+ .trigger('mouseover')
+ .children('.slick-header-menu-button')
+ .invoke('show')
+ .click();
+
+ cy.get('[data-command=filter-shortcuts-root-menu]')
+ .should('contain', 'Filter Shortcuts')
+ .trigger('mouseover');
+
+ cy.get('.slick-header-menu.slick-menu-level-1')
+ .find('[data-command=in-the-future]')
+ .click();
+
+ cy.get('.search-filter.filter-finish .input-group-prepend.operator select')
+ .contains('>=');
+
+ cy.get('.search-filter.filter-finish input.date-picker')
+ .invoke('val')
+ .should('equal', format(addDay(new Date(), 1), 'YYYY-MM-DD'));
+
+ cy.get('.grid11')
+ .find('.slick-custom-footer')
+ .find('.right-footer .item-count')
+ .should($span => {
+ expect(Number($span.text())).to.lt(969);
+ });
+ });
});
describe('Local Storage', () => {
diff --git a/test/translateServiceStub.ts b/test/translateServiceStub.ts
index 6c5396183..9a5b3134b 100644
--- a/test/translateServiceStub.ts
+++ b/test/translateServiceStub.ts
@@ -43,6 +43,7 @@ export class TranslateServiceStub implements TranslaterService {
case 'EXPORT_TO_EXCEL': output = this._locale === 'en' ? 'Export to Excel' : 'Exporter vers Excel'; break;
case 'EXPORT_TO_TAB_DELIMITED': output = this._locale === 'en' ? 'Export in Text format (Tab delimited)' : 'Exporter en format texte (délimité par tabulation)'; break;
case 'EXPORT_TO_TEXT_FORMAT': output = this._locale === 'en' ? 'Export in Text format' : 'Exporter en format texte'; break;
+ case 'FILTER_SHORTCUTS': output = this._locale === 'en' ? 'Filter Shortcuts' : 'Raccourcis de filtre'; break;
case 'FEMALE': output = this._locale === 'en' ? 'Female' : 'Femme'; break;
case 'FIRST_NAME': output = this._locale === 'en' ? 'First Name' : 'Prénom'; break;
case 'FORCE_FIT_COLUMNS': output = this._locale === 'en' ? 'Force fit columns' : 'Ajustement forcé des colonnes'; break;