From b5a468c44b5ff8b23f41a4237110f3550b618cff Mon Sep 17 00:00:00 2001 From: Maxim Kudryavtsev Date: Thu, 21 Jun 2018 12:00:07 +0300 Subject: [PATCH] feat(vue-grid): add the Advanced filtering feature (#1186) --- .../docs/reference/table-filter-row.md | 2 + .../none/demo.js | 4 +- .../none/advanced-filter-row.js | 165 ++++++++++++++++ .../grid-filtering/none/custom-filter-row.js | 14 +- .../none/components/currency-type-provider.js | 11 ++ .../none/components/percent-type-provider.js | 11 ++ .../src/plugins/table-filter-row.js | 31 ++- .../src/templates/filter-row/editor.js | 42 +++++ .../src/templates/filter-row/editor.test.js | 44 +++++ .../templates/filter-row/filter-selector.js | 87 +++++++++ .../filter-row/filter-selector.test.js | 55 ++++++ .../src/templates/filter-row/icon.js | 40 ++++ .../src/templates/filter-row/icon.test.js | 16 ++ .../src/templates/popover.js | 39 ++-- .../src/templates/popover.test.js | 36 +++- .../src/templates/table-filter-cell.js | 16 +- .../src/templates/table-filter-cell.test.js | 48 ----- .../src/templates/virtual-table-layout.js | 2 +- packages/dx-vue-grid/docs/guides/filtering.md | 12 +- .../docs/reference/integrated-filtering.md | 6 + .../docs/reference/table-filter-row.md | 60 ++++++ .../dx-vue-grid/src/plugins/column-chooser.js | 2 +- .../src/plugins/data-type-provider.js | 16 +- .../src/plugins/integrated-filtering.js | 3 + .../src/plugins/table-filter-row.js | 103 ++++++++-- .../src/plugins/table-filter-row.test.js | 177 +++++++++++++++++- 26 files changed, 932 insertions(+), 110 deletions(-) create mode 100644 packages/dx-vue-demos/src/demo-sources/grid-filtering/none/advanced-filter-row.js create mode 100644 packages/dx-vue-grid-bootstrap4/src/templates/filter-row/editor.js create mode 100644 packages/dx-vue-grid-bootstrap4/src/templates/filter-row/editor.test.js create mode 100644 packages/dx-vue-grid-bootstrap4/src/templates/filter-row/filter-selector.js create mode 100644 packages/dx-vue-grid-bootstrap4/src/templates/filter-row/filter-selector.test.js create mode 100644 packages/dx-vue-grid-bootstrap4/src/templates/filter-row/icon.js create mode 100644 packages/dx-vue-grid-bootstrap4/src/templates/filter-row/icon.test.js diff --git a/packages/dx-react-grid/docs/reference/table-filter-row.md b/packages/dx-react-grid/docs/reference/table-filter-row.md index 5a954aff66..41cdfc355c 100644 --- a/packages/dx-react-grid/docs/reference/table-filter-row.md +++ b/packages/dx-react-grid/docs/reference/table-filter-row.md @@ -65,6 +65,8 @@ iconComponent | ComponentType<[TableFilterRow.IconProps](#tablefilterrowiconp value | string | The currently selected filter operation. availableValues | Array<string> | The list of available filter operations. onChange | (value: string) => void | Handles filter operation changes. +disabled | boolean | Specifies whether the FilterSelector is disabled. +getMessage | ([messageKey](#localization-messages): string) => string | Returns the specified localization message. ### TableFilterRow.IconProps diff --git a/packages/dx-vue-demos/src/demo-sources/grid-featured-integrated-data-shaping/none/demo.js b/packages/dx-vue-demos/src/demo-sources/grid-featured-integrated-data-shaping/none/demo.js index 9997aac5ee..3fb5433471 100644 --- a/packages/dx-vue-demos/src/demo-sources/grid-featured-integrated-data-shaping/none/demo.js +++ b/packages/dx-vue-demos/src/demo-sources/grid-featured-integrated-data-shaping/none/demo.js @@ -137,7 +137,9 @@ export default { showSortingControls showGroupingControls /> - + diff --git a/packages/dx-vue-demos/src/demo-sources/grid-filtering/none/advanced-filter-row.js b/packages/dx-vue-demos/src/demo-sources/grid-filtering/none/advanced-filter-row.js new file mode 100644 index 0000000000..6bdec094e4 --- /dev/null +++ b/packages/dx-vue-demos/src/demo-sources/grid-filtering/none/advanced-filter-row.js @@ -0,0 +1,165 @@ +import { + DxDataTypeProvider, + DxFilteringState, + DxIntegratedFiltering, +} from '@devexpress/dx-vue-grid'; +import { + DxGrid, + DxTable, + DxTableHeaderRow, + DxTableFilterRow, +} from '@devexpress/dx-vue-grid-bootstrap4'; + +import { + generateRows, + globalSalesValues, +} from '../../../demo-data/generator'; + +const MyDateFilterCell = { + template: ` + + `, +}; + +const FilterIcon = { + props: ['type'], + computed: { + componentId() { + return this.type === 'month' ? 'my-date-filter-cell' : 'dx-icon'; + }, + }, + template: ` + + `, + components: { + DxIcon: DxTableFilterRow.components.DxIcon, + MyDateFilterCell, + }, +}; + +const CurrencyFormatter = { + props: ['value'], + template: ` + \${{ value }} + `, +}; + +const CurrencyTypeProvider = { + template: ` + + `, + components: { + DxDataTypeProvider, + CurrencyFormatter, + }, +}; + +const DateFormatter = { + props: ['value'], + template: ` + {{ value.replace(/(\\d{4})-(\\d{2})-(\\d{2})/, '$3.$2.$1') }} + `, +}; + +const DateTypeProvider = { + template: ` + + `, + components: { + DxDataTypeProvider, + DateFormatter, + }, +}; + +export default { + data() { + return { + columns: [ + { name: 'customer', title: 'Customer' }, + { name: 'product', title: 'Product' }, + { name: 'saleDate', title: 'Sale Date' }, + { name: 'amount', title: 'Sale Amount' }, + ], + rows: generateRows({ columnValues: globalSalesValues, length: 8 }), + tableColumnExtensions: [ + { columnName: 'amount', align: 'right' }, + ], + filters: [], + dateColumns: ['saleDate'], + currencyColumns: ['amount'], + dateFilterOperations: ['month', 'contains', 'startsWith', 'endsWith'], + currencyFilterOperations: ['equal', 'notEqual', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual'], + filteringColumnExtensions: [ + { + columnName: 'saleDate', + predicate: (value, filter, row) => { + if (!filter.value.length) return true; + if (filter && filter.operation === 'month') { + const month = parseInt(value.split('-')[1], 10); + return month === parseInt(filter.value, 10); + } + return DxIntegratedFiltering.defaultPredicate(value, filter, row); + }, + }, + ], + filterMessages: { month: 'Month equals' }, + }; + }, + template: ` +
+ + + + + + + + + + +
+ `, + components: { + DxGrid, + DxTable, + DxTableHeaderRow, + CurrencyTypeProvider, + DateTypeProvider, + DxFilteringState, + DxIntegratedFiltering, + DxTableFilterRow, + FilterIcon, + }, +}; diff --git a/packages/dx-vue-demos/src/demo-sources/grid-filtering/none/custom-filter-row.js b/packages/dx-vue-demos/src/demo-sources/grid-filtering/none/custom-filter-row.js index 2777921a60..b1364a4a18 100644 --- a/packages/dx-vue-demos/src/demo-sources/grid-filtering/none/custom-filter-row.js +++ b/packages/dx-vue-demos/src/demo-sources/grid-filtering/none/custom-filter-row.js @@ -18,7 +18,7 @@ const MyUnitsFilterCell = { inheritAttrs: false, props: ['filter'], template: ` - @@ -28,18 +28,16 @@ const MyUnitsFilterCell = { :value="filter ? filter.value : ''" min="1" max="4" + placeholder="Filter..." @change="e => this.$emit('filter', e.target.value ? { value: e.target.value } : null)" /> - + `, - components: { - DxCell: DxTableFilterRow.components.DxCell, - }, }; const MyFilterCell = { inheritAttrs: false, - props: ['column'], + props: ['column', 'getMessage'], data() { return { componentId: this.column.name === 'units' ? 'my-units-filter-cell' : 'dx-cell', @@ -51,7 +49,9 @@ const MyFilterCell = { :column="column" v-bind="$attrs" v-on="$listeners" - /> + > + +
`, components: { DxCell: DxTableFilterRow.components.DxCell, diff --git a/packages/dx-vue-demos/src/theme-sources/none/components/currency-type-provider.js b/packages/dx-vue-demos/src/theme-sources/none/components/currency-type-provider.js index 1878dd7b6d..b91bfa44ae 100644 --- a/packages/dx-vue-demos/src/theme-sources/none/components/currency-type-provider.js +++ b/packages/dx-vue-demos/src/theme-sources/none/components/currency-type-provider.js @@ -23,6 +23,7 @@ const CurrencyEditor = { style="'width': '100%'" :value="value === undefined ? '' : value" min="0" + placeholder="Filter..." @change="handleChange" /> `, @@ -42,10 +43,20 @@ const CurrencyFormatter = { }; export const CurrencyTypeProvider = { + data() { + return ({ + availableFilterOperations: [ + 'equal', 'notEqual', + 'greaterThan', 'greaterThanOrEqual', + 'lessThan', 'lessThanOrEqual', + ], + }); + }, template: ` `, diff --git a/packages/dx-vue-demos/src/theme-sources/none/components/percent-type-provider.js b/packages/dx-vue-demos/src/theme-sources/none/components/percent-type-provider.js index 1c2eb07402..bc6606ab27 100644 --- a/packages/dx-vue-demos/src/theme-sources/none/components/percent-type-provider.js +++ b/packages/dx-vue-demos/src/theme-sources/none/components/percent-type-provider.js @@ -25,15 +25,26 @@ const PercentEditor = { step="0.1" min="0" max="100" + placeholder="Filter..." @change="handleChange" /> `, }; export const PercentTypeProvider = { + data() { + return ({ + availableFilterOperations: [ + 'equal', 'notEqual', + 'greaterThan', 'greaterThanOrEqual', + 'lessThan', 'lessThanOrEqual', + ], + }); + }, template: ` `, diff --git a/packages/dx-vue-grid-bootstrap4/src/plugins/table-filter-row.js b/packages/dx-vue-grid-bootstrap4/src/plugins/table-filter-row.js index d28c967cf2..b630d65641 100644 --- a/packages/dx-vue-grid-bootstrap4/src/plugins/table-filter-row.js +++ b/packages/dx-vue-grid-bootstrap4/src/plugins/table-filter-row.js @@ -1,21 +1,50 @@ import { DxTableFilterRow as DxTableFilterRowBase } from '@devexpress/dx-vue-grid'; import { TableFilterCell } from '../templates/table-filter-cell'; import { TableRow } from '../templates/table-row'; +import { Editor } from '../templates/filter-row/editor'; +import { FilterSelector } from '../templates/filter-row/filter-selector'; +import { Icon } from '../templates/filter-row/icon'; + +const defaultMessages = { + filterPlaceholder: 'Filter...', + contains: 'Contains', + notContains: 'Does not contain', + startsWith: 'Starts with', + endsWith: 'Ends with', + equal: 'Equals', + notEqual: 'Does not equal', + greaterThan: 'Greater than', + greaterThanOrEqual: 'Greater than or equal to', + lessThan: 'Less than', + lessThanOrEqual: 'Less than or equal to', +}; export const DxTableFilterRow = { name: 'DxTableFilterRow', functional: true, + props: { + messages: { + type: Object, + }, + }, render(h, context) { return ( ); }, components: { DxCell: TableFilterCell, DxRow: TableRow, + DxEditor: Editor, + DxFilterSelector: FilterSelector, + DxIcon: Icon, }, }; diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/editor.js b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/editor.js new file mode 100644 index 0000000000..7bfa14c910 --- /dev/null +++ b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/editor.js @@ -0,0 +1,42 @@ +export const Editor = { + name: 'Editor', + inheritAttrs: false, + props: { + value: { + type: [Number, String], + default: '', + }, + disabled: { + type: Boolean, + default: false, + }, + getMessage: { + type: Function, + required: true, + }, + }, + methods: { + handleChange(e) { + this.$emit('changeValue', e.target.value); + }, + }, + render() { + const { + value, + disabled, + getMessage, + } = this; + + return ( + + ); + }, +}; diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/editor.test.js b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/editor.test.js new file mode 100644 index 0000000000..d17c06f5ea --- /dev/null +++ b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/editor.test.js @@ -0,0 +1,44 @@ +import { shallow } from '@vue/test-utils'; +import { Editor } from './editor'; + +const defaultProps = { + getMessage: key => key, +}; + +describe('Editor', () => { + it('should render a readonly input if disabled', () => { + const tree = shallow(({ + render() { + return ( + + ); + }, + })); + + expect(tree.find('input').element.readOnly) + .toBeTruthy(); + }); + + it('should trigger changeValue when change event fire', () => { + const onValueChange = jest.fn(); + const tree = shallow(({ + render() { + return ( + + ); + }, + })); + + const input = tree.find('input'); + + input.element.value = 'abc'; + input.trigger('input'); + expect(onValueChange).toBeCalledWith('abc'); + }); +}); diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/filter-selector.js b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/filter-selector.js new file mode 100644 index 0000000000..f9a6c62ffc --- /dev/null +++ b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/filter-selector.js @@ -0,0 +1,87 @@ +import { Popover } from '../popover'; + +export const FilterSelector = { + name: 'FilterSelector', + props: { + value: { + type: String, + }, + availableValues: { + type: Array, + default: () => [], + }, + disabled: { + type: Boolean, + default: false, + }, + getMessage: { + type: Function, + required: true, + }, + iconComponent: { + type: Object, + required: true, + }, + }, + data() { + return ({ + opened: false, + }); + }, + methods: { + handleButtonClick(e) { + e.stopPropagation(); + this.opened = !this.opened; + }, + handleOverlayToggle() { + if (this.opened) this.opened = false; + }, + handleMenuItemClick(nextValue) { + this.opened = false; + this.$emit('changeValue', nextValue); + }, + }, + render() { + const { + value, availableValues, disabled, getMessage, iconComponent: Icon, opened, + } = this; + return availableValues.length ? ( +
+ + {this.$refs.buttonRef ? ( + +
+ {availableValues.map(valueItem => ( + + ))} +
+
+ ) : null} +
+ ) : null; + }, +}; diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/filter-selector.test.js b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/filter-selector.test.js new file mode 100644 index 0000000000..aa0180642f --- /dev/null +++ b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/filter-selector.test.js @@ -0,0 +1,55 @@ +import { shallow } from '@vue/test-utils'; +import { FilterSelector } from './filter-selector'; + +const defaultProps = { + iconComponent: { name: 'Icon', render() { return null; } }, + getMessage: key => key, +}; + +describe('FilterSelector', () => { + it('should not render anything if no values are available', () => { + const tree = shallow(({ + render() { + return ( + + ); + }, + })); + + expect(tree.find('.input-group-prepend')) + .toBeTruthy(); + }); + + it('should render the disabled toggle button if only one value is available', () => { + const tree = shallow(({ + render() { + return ( + + ); + }, + })); + + expect(tree.find('button').element.disabled) + .toBeTruthy(); + }); + + it('should render the disabled toggle button if the "disabled" prop is true', () => { + const tree = shallow(({ + render() { + return ( + + ); + }, + })); + + expect(tree.find('button').element.disabled) + .toBeTruthy(); + }); +}); diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/icon.js b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/icon.js new file mode 100644 index 0000000000..ecfe38d926 --- /dev/null +++ b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/icon.js @@ -0,0 +1,40 @@ +const AVAILABLE_PATHS = { + contains: 'M6.094 19.563l-2.031 0.281c-0.646 0.094-1.13 0.266-1.453 0.516-0.302 0.24-0.453 0.646-0.453 1.219 0 0.438 0.138 0.799 0.414 1.086s0.664 0.419 1.164 0.398c0.708 0 1.281-0.24 1.719-0.719 0.427-0.49 0.641-1.125 0.641-1.906v-0.875zM8.234 24.641h-2.172v-1.641c-0.677 1.24-1.661 1.859-2.953 1.859-0.927 0-1.682-0.276-2.266-0.828-0.552-0.552-0.828-1.292-0.828-2.219 0-1.927 1.068-3.052 3.203-3.375l2.875-0.438c0-1.469-0.656-2.203-1.969-2.203-1.177 0-2.224 0.427-3.141 1.281v-2.078c1.010-0.656 2.198-0.984 3.563-0.984 2.458 0 3.687 1.302 3.687 3.906v6.719zM14.734 16.797c0.521-0.583 1.167-0.875 1.938-0.875 0.74 0 1.323 0.281 1.75 0.844 0.448 0.583 0.672 1.38 0.672 2.391 0 1.188-0.24 2.13-0.719 2.828-0.49 0.677-1.13 1.016-1.922 1.016-0.719 0-1.302-0.271-1.75-0.813-0.427-0.51-0.641-1.141-0.641-1.891v-1.266c-0.021-0.906 0.203-1.651 0.672-2.234zM16.969 24.859c1.375 0 2.443-0.521 3.203-1.562 0.781-1.042 1.172-2.427 1.172-4.156 0-1.542-0.354-2.771-1.063-3.688-0.688-0.958-1.651-1.438-2.891-1.438-1.427 0-2.531 0.693-3.313 2.078v-6.781h-2.156v15.328h2.172v-1.5c0.677 1.146 1.635 1.719 2.875 1.719zM22.266 6.125c0.135 0 0.245 0.063 0.328 0.188 0.104 0.073 0.156 0.182 0.156 0.328v22.953c0 0.125-0.052 0.24-0.156 0.344-0.083 0.115-0.193 0.172-0.328 0.172h-12.281c-0.146 0-0.266-0.057-0.359-0.172-0.115-0.115-0.172-0.229-0.172-0.344v-22.953c0-0.135 0.057-0.245 0.172-0.328 0.094-0.125 0.214-0.188 0.359-0.188h12.281zM31.531 24.141c-0.76 0.479-1.693 0.719-2.797 0.719-1.427 0-2.589-0.479-3.484-1.438-0.865-0.958-1.286-2.198-1.266-3.719 0-1.688 0.448-3.052 1.344-4.094 0.917-1.042 2.208-1.573 3.875-1.594 0.854 0 1.63 0.177 2.328 0.531v2.156c-0.677-0.531-1.391-0.792-2.141-0.781-0.938 0-1.714 0.339-2.328 1.016-0.594 0.677-0.891 1.552-0.891 2.625 0 1.042 0.297 1.88 0.891 2.516 0.521 0.615 1.25 0.922 2.188 0.922 0.813 0 1.573-0.297 2.281-0.891v2.031z', + notContains: 'M5.828 20.469v0.328c0 0.385-0.057 0.667-0.172 0.844-0.052 0.083-0.117 0.177-0.195 0.281s-0.174 0.224-0.289 0.359c-0.458 0.521-1.031 0.771-1.719 0.75-0.521 0-0.927-0.141-1.219-0.422-0.292-0.292-0.438-0.661-0.438-1.109 0-0.156 0.010-0.273 0.031-0.352s0.052-0.141 0.094-0.188 0.094-0.086 0.156-0.117 0.141-0.078 0.234-0.141c0.031-0.031 0.078-0.070 0.141-0.117s0.146-0.086 0.25-0.117h3.125zM14.016 18.328c0.010-0.406 0.070-0.729 0.18-0.969s0.289-0.49 0.539-0.75c0.479-0.604 1.13-0.906 1.953-0.906 0.75 0 1.344 0.292 1.781 0.875 0.198 0.25 0.349 0.495 0.453 0.734s0.172 0.578 0.203 1.016h-5.109zM19.078 20.469c-0.063 0.427-0.146 0.708-0.25 0.844-0.052 0.073-0.109 0.159-0.172 0.258l-0.219 0.352c-0.469 0.688-1.135 1.031-2 1.031-0.708 0-1.297-0.271-1.766-0.813l-0.305-0.359c-0.089-0.104-0.159-0.198-0.211-0.281-0.104-0.167-0.156-0.448-0.156-0.844v-0.188h5.078zM33.344 18.328l-6.875 0c0.031-0.198 0.070-0.372 0.117-0.523s0.107-0.284 0.18-0.398 0.154-0.224 0.242-0.328l0.305-0.344c0.604-0.688 1.391-1.031 2.359-1.031 0.771 0 1.51 0.266 2.219 0.797v-2.234c-0.75-0.333-1.552-0.5-2.406-0.5-1.667 0-2.974 0.531-3.922 1.594-0.396 0.427-0.708 0.859-0.938 1.297s-0.385 0.995-0.469 1.672h-2.719c-0.021-0.719-0.117-1.31-0.289-1.773s-0.424-0.914-0.758-1.352c-0.729-0.938-1.719-1.417-2.969-1.438-1.479 0-2.615 0.708-3.406 2.125v-6.953h-2.266v9.391h-3.75v-0.594c0-2.646-1.25-3.969-3.75-3.969-1.365 0-2.583 0.328-3.656 0.984v2.125c0.99-0.865 2.063-1.297 3.219-1.297 1.344 0 2.016 0.75 2.016 2.25l-2.953 0.125c-0.25 0.021-0.487 0.070-0.711 0.148l-0.633 0.227h-3.328v2.141h1.828l-0.281 0.594c-0.073 0.135-0.109 0.37-0.109 0.703 0 0.938 0.276 1.682 0.828 2.234 0.542 0.573 1.313 0.859 2.313 0.859 1.281 0 2.297-0.635 3.047-1.906v1.656h2.172v-4.141h3.75v4.141h2.297v-1.516c0.677 1.188 1.661 1.776 2.953 1.766 1.385 0 2.464-0.531 3.234-1.594 0.302-0.385 0.557-0.792 0.766-1.219 0.198-0.385 0.339-0.911 0.422-1.578h2.703c0.021 0.708 0.141 1.25 0.359 1.625 0.115 0.198 0.253 0.401 0.414 0.609s0.346 0.427 0.555 0.656c0.906 1 2.099 1.5 3.578 1.5 1.104 0 2.057-0.245 2.859-0.734v-2.109c-0.75 0.604-1.526 0.917-2.328 0.938-0.979 0-1.74-0.318-2.281-0.953l-0.328-0.328c-0.094-0.094-0.177-0.195-0.25-0.305s-0.13-0.234-0.172-0.375-0.073-0.315-0.094-0.523h6.906v-2.141zM33.297 5.688c0.146 0 0.266 0.047 0.359 0.141 0.104 0.104 0.156 0.229 0.156 0.375v23.484c0 0.135-0.052 0.255-0.156 0.359-0.094 0.115-0.214 0.172-0.359 0.172h-35.078c-0.135 0-0.26-0.057-0.375-0.172-0.094-0.115-0.135-0.234-0.125-0.359v-23.484c0-0.104 0.042-0.229 0.125-0.375 0.104-0.094 0.229-0.141 0.375-0.141h35.078z', + startsWith: 'M6.109 20.688c0 0.813-0.219 1.474-0.656 1.984-0.448 0.531-1.010 0.786-1.688 0.766-0.51 0-0.896-0.141-1.156-0.422-0.302-0.292-0.443-0.667-0.422-1.125 0-0.615 0.151-1.042 0.453-1.281 0.177-0.135 0.378-0.245 0.602-0.328s0.497-0.146 0.82-0.188l2.047-0.313v0.906zM8.203 18.063c0-2.688-1.219-4.031-3.656-4.031-1.333 0-2.51 0.339-3.531 1.016v2.141c0.917-0.885 1.948-1.328 3.094-1.328 1.333 0 2 0.766 2 2.297l-2.891 0.453c-2.115 0.333-3.161 1.516-3.141 3.547 0 0.958 0.266 1.724 0.797 2.297 0.542 0.573 1.292 0.859 2.25 0.859 1.292 0 2.26-0.641 2.906-1.922v1.688h2.172v-7.016zM14.703 16.906c0.479-0.604 1.109-0.906 1.891-0.906 0.76 0 1.344 0.297 1.75 0.891 0.438 0.615 0.656 1.443 0.656 2.484 0 1.219-0.229 2.198-0.688 2.938-0.469 0.719-1.109 1.078-1.922 1.078-0.719 0-1.286-0.281-1.703-0.844-0.448-0.542-0.672-1.208-0.672-2v-1.313c-0.010-0.938 0.219-1.714 0.688-2.328zM16.906 25.313c1.365 0 2.422-0.542 3.172-1.625 0.771-1.115 1.156-2.563 1.156-4.344 0-1.604-0.339-2.885-1.016-3.844-0.698-0.979-1.661-1.469-2.891-1.469-1.438 0-2.531 0.719-3.281 2.156v-7.078h-2.188v15.969h2.172v-1.563c0.667 1.198 1.625 1.797 2.875 1.797zM31.375 24.563c-0.75 0.5-1.672 0.75-2.766 0.75-1.427 0-2.583-0.505-3.469-1.516-0.885-0.969-1.318-2.26-1.297-3.875 0-1.74 0.464-3.161 1.391-4.266 0.927-1.063 2.198-1.604 3.813-1.625 0.844 0 1.62 0.172 2.328 0.516v2.25c-0.688-0.563-1.406-0.828-2.156-0.797-0.927 0-1.688 0.349-2.281 1.047-0.583 0.698-0.875 1.609-0.875 2.734 0 1.094 0.281 1.969 0.844 2.625 0.542 0.656 1.286 0.984 2.234 0.984 0.781 0 1.526-0.323 2.234-0.969v2.141zM22.172 5.844c0.115 0 0.224 0.052 0.328 0.156 0.094 0.125 0.141 0.25 0.141 0.375v23.844c0 0.156-0.047 0.286-0.141 0.391-0.115 0.094-0.224 0.141-0.328 0.141h-23.469c-0.125 0-0.24-0.047-0.344-0.141-0.094-0.104-0.141-0.234-0.141-0.391v-23.844c0-0.125 0.047-0.25 0.141-0.375 0.104-0.104 0.219-0.156 0.344-0.156h23.469z', + endsWith: 'M6.234 19.344l-2.047 0.313c-0.625 0.083-1.104 0.26-1.438 0.531-0.302 0.24-0.453 0.651-0.453 1.234 0 0.469 0.141 0.852 0.422 1.148s0.672 0.435 1.172 0.414c0.677 0 1.234-0.25 1.672-0.75 0.448-0.51 0.672-1.167 0.672-1.969v-0.922zM8.359 24.578h-2.141v-1.656c-0.667 1.26-1.656 1.891-2.969 1.891-0.938 0-1.698-0.276-2.281-0.828-0.542-0.573-0.813-1.328-0.813-2.266 0-2.021 1.063-3.188 3.188-3.5l2.891-0.484c0-1.51-0.661-2.266-1.984-2.266-1.167 0-2.214 0.443-3.141 1.328v-2.125c1.042-0.677 2.224-1.016 3.547-1.016 2.469 0 3.703 1.333 3.703 4v6.922zM14.906 16.516c0.49-0.615 1.13-0.922 1.922-0.922 0.76 0 1.339 0.297 1.734 0.891 0.438 0.615 0.656 1.438 0.656 2.469 0 1.208-0.229 2.182-0.688 2.922-0.469 0.698-1.115 1.047-1.938 1.047-0.708 0-1.276-0.276-1.703-0.828-0.458-0.552-0.688-1.214-0.688-1.984v-1.281c-0.010-0.948 0.224-1.719 0.703-2.313zM17.125 24.813c1.354 0 2.417-0.531 3.188-1.594 0.781-1.073 1.172-2.505 1.172-4.297 0-1.604-0.349-2.87-1.047-3.797-0.698-0.979-1.661-1.469-2.891-1.469-1.438 0-2.542 0.714-3.313 2.141v-7h-2.203v15.781h2.188v-1.531c0.677 1.177 1.646 1.766 2.906 1.766zM31.688 21.969c-0.698 0.635-1.453 0.953-2.266 0.953-0.958 0-1.703-0.323-2.234-0.969-0.563-0.667-0.849-1.536-0.859-2.609 0-1.115 0.297-2.016 0.891-2.703 0.594-0.698 1.359-1.047 2.297-1.047 0.76 0 1.484 0.266 2.172 0.797v-2.219c-0.708-0.344-1.49-0.516-2.344-0.516-1.625 0-2.906 0.536-3.844 1.609-0.938 1.083-1.406 2.495-1.406 4.234 0 1.594 0.438 2.875 1.313 3.844 0.885 0.979 2.052 1.469 3.5 1.469 1.083 0 2.010-0.245 2.781-0.734v-2.109zM33.188 5.563c0.104 0 0.219 0.047 0.344 0.141 0.094 0.146 0.141 0.276 0.141 0.391v23.578c0 0.146-0.047 0.281-0.141 0.406-0.125 0.094-0.24 0.141-0.344 0.141h-23.625c-0.125 0-0.24-0.047-0.344-0.141-0.094-0.135-0.135-0.271-0.125-0.406v-23.578c0-0.115 0.042-0.245 0.125-0.391 0.094-0.094 0.208-0.141 0.344-0.141h23.625z', + equal: 'M29.438 11.797v2.75h-26.922v-2.75h26.922zM29.438 17.406v2.75h-26.922v-2.75h26.922z', + notEqual: 'M16.906 11.797l3.016-6.547 2.094 1-2.547 5.547h9.969v2.75h-11.234l-1.328 2.859h12.563v2.75h-13.828l-2.875 6.281-2.094-0.984 2.438-5.297h-10.563v-2.75h11.828l1.297-2.859h-13.125v-2.75h14.391z', + greaterThan: 'M24.125 16.047l-14.906 8.625-1.375-2.375 10.781-6.25-10.781-6.234 1.375-2.375z', + greaterThanOrEqual: 'M23.031 14.328l-14.906 8.625-1.375-2.375 10.797-6.25-10.797-6.234 1.375-2.375zM23.828 15.641l1.375 2.391-14.938 8.609-1.375-2.375z', + lessThan: 'M22.75 7.438l1.375 2.375-10.781 6.234 10.781 6.25-1.375 2.375-14.906-8.609z', + lessThanOrEqual: 'M23.828 5.719l1.375 2.375-10.813 6.234 10.813 6.25-1.375 2.375-14.922-8.609zM23.047 24.266l-1.375 2.375-14.922-8.609 1.375-2.391z', +}; + +export const Icon = { + name: 'Icon', + props: { + type: { + type: String, + }, + }, + render() { + const path = AVAILABLE_PATHS[this.type]; + return path + ? ( + + + + ) + : ( + + ); + }, +}; diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/icon.test.js b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/icon.test.js new file mode 100644 index 0000000000..fa6f2baa6d --- /dev/null +++ b/packages/dx-vue-grid-bootstrap4/src/templates/filter-row/icon.test.js @@ -0,0 +1,16 @@ +import { shallow } from '@vue/test-utils'; +import { Icon } from './icon'; + +describe('Icon', () => { + it('should render default icon if unknown type is specified', () => { + const tree = shallow(({ + render() { + return ( + + ); + }, + })); + expect(tree.find('.oi-magnifying-glass').exists()) + .toBeTruthy(); + }); +}); diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/popover.js b/packages/dx-vue-grid-bootstrap4/src/templates/popover.js index c786b0daa7..bc03224e9f 100644 --- a/packages/dx-vue-grid-bootstrap4/src/templates/popover.js +++ b/packages/dx-vue-grid-bootstrap4/src/templates/popover.js @@ -1,5 +1,5 @@ -const offsetX = 3; -const offsetY = 3; +const offsetX = 5; +const offsetY = 5; export const Popover = { name: 'Popover', props: { @@ -19,6 +19,24 @@ export const Popover = { this.$emit('toggle', e); } }, + setElementTranslate() { + const { + height: targetHeight, + width: targetWidth, + left: targetLeft, + } = this.target.getBoundingClientRect(); + const { container = document.body, width } = this; + const popoverWidth = width || this.$el.offsetWidth; + let x = (targetWidth - popoverWidth) / 2; + const popoverRight = targetLeft + ((targetWidth + popoverWidth) / 2); + if (popoverRight > container.offsetWidth) { + x -= (popoverRight - container.offsetWidth) + offsetX; + } + if ((targetLeft - Math.abs(x)) < 0) { + x = offsetX - targetLeft; + } + this.$el.style.transform = `translate(${x}px, ${targetHeight + offsetY}px)`; + }, }, created() { document.addEventListener('click', this.handleDocumentClick); @@ -27,19 +45,10 @@ export const Popover = { document.removeEventListener('click', this.handleDocumentClick); }, mounted() { - const { - bottom, - left, - width: targetWidth, - } = this.target.getBoundingClientRect(); - const { container = document.body, width } = this; - const popoverWidth = width || this.$el.offsetWidth; - let x = (left + (targetWidth / 2)) - (popoverWidth / 2); - const delta = container.offsetWidth - (x + popoverWidth); - if (delta < 0) { - x += delta - offsetX; - } - this.$el.style.transform = `translate(${x}px, ${bottom + offsetY}px)`; + this.setElementTranslate(); + }, + updated() { + this.setElementTranslate(); }, render() { return ( diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/popover.test.js b/packages/dx-vue-grid-bootstrap4/src/templates/popover.test.js index c72eb5ee64..c643cc172f 100644 --- a/packages/dx-vue-grid-bootstrap4/src/templates/popover.test.js +++ b/packages/dx-vue-grid-bootstrap4/src/templates/popover.test.js @@ -4,7 +4,7 @@ import { Popover } from './popover'; const defaultProps = { target: { getBoundingClientRect: () => ({ - bottom: 10, + height: 10, left: 100, width: 20, }), @@ -100,17 +100,17 @@ describe('Popover', () => { }); expect(wrapper.element.style.transform) - .toBe('translate(85px, 13px)'); + .toBe('translate(-15px, 15px)'); }); - it('should calculate position shift depend on container size', () => { + it('should calculate position shift depend on container right size', () => { const wrapper = shallow({ render() { return ( ({ - bottom: 10, + height: 10, left: 290, width: 20, }), @@ -126,6 +126,32 @@ describe('Popover', () => { }); expect(wrapper.element.style.transform) - .toBe('translate(247px, 13px)'); + .toBe('translate(-45px, 15px)'); + }); + + it('should calculate position shift depend on container left size', () => { + const wrapper = shallow({ + render() { + return ( + ({ + height: 10, + left: 10, + width: 20, + }), + }} + container={{ + offsetWidth: 300, + }} + width={50} + visible + /> + ); + }, + }); + + expect(wrapper.element.style.transform) + .toBe('translate(-5px, 15px)'); }); }); diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/table-filter-cell.js b/packages/dx-vue-grid-bootstrap4/src/templates/table-filter-cell.js index 64400365bd..bb2a37002d 100644 --- a/packages/dx-vue-grid-bootstrap4/src/templates/table-filter-cell.js +++ b/packages/dx-vue-grid-bootstrap4/src/templates/table-filter-cell.js @@ -1,4 +1,5 @@ export const TableFilterCell = { + name: 'TableFilterCell', props: { column: { type: Object, @@ -22,18 +23,11 @@ export const TableFilterCell = { }, }, render() { - const { filter, filteringEnabled } = this; return ( - - {this.$slots.default || ( - this.$emit('filter', e.target.value ? { value: e.target.value } : null)} - readonly={!filteringEnabled} - /> - )} + +
+ {this.$slots.default} +
); }, diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/table-filter-cell.test.js b/packages/dx-vue-grid-bootstrap4/src/templates/table-filter-cell.test.js index 7d0f31eff8..0ff36ad6b5 100644 --- a/packages/dx-vue-grid-bootstrap4/src/templates/table-filter-cell.test.js +++ b/packages/dx-vue-grid-bootstrap4/src/templates/table-filter-cell.test.js @@ -2,28 +2,6 @@ import { shallow } from '@vue/test-utils'; import { TableFilterCell } from './table-filter-cell'; describe('TableFilterCell', () => { - it('should not set filter with an empty value', () => { - const onFilterMock = jest.fn(); - const tree = shallow({ - render() { - return ( - - ); - }, - }); - - const input = tree.find('input'); - input.element.value = ''; - input.trigger('input'); - expect(onFilterMock.mock.calls[0][0]).toBeNull(); - }); - it('should render children if passed', () => { const tree = shallow({ render() { @@ -39,19 +17,6 @@ describe('TableFilterCell', () => { .toBeTruthy(); }); - it('should pass rest props to the root element', () => { - const tree = shallow({ - render() { - return ( - - ); - }, - }); - - expect(tree.is('.custom-class')) - .toBeTruthy(); - }); - it('should pass rest props to the root element', () => { const tree = shallow({ render() { @@ -63,18 +28,5 @@ describe('TableFilterCell', () => { expect(tree.attributes().data) .toBe('abc'); }); - - it('should render readonly filtering editor if filtering is not allowed', () => { - const tree = shallow({ - render() { - return ( - key} /> - ); - }, - }); - - expect(tree.find('input').attributes().readonly) - .toBeTruthy(); - }); }); diff --git a/packages/dx-vue-grid-bootstrap4/src/templates/virtual-table-layout.js b/packages/dx-vue-grid-bootstrap4/src/templates/virtual-table-layout.js index d973445e7b..fbf17a7ae0 100644 --- a/packages/dx-vue-grid-bootstrap4/src/templates/virtual-table-layout.js +++ b/packages/dx-vue-grid-bootstrap4/src/templates/virtual-table-layout.js @@ -13,7 +13,7 @@ export const VirtualTableLayout = { ); }, diff --git a/packages/dx-vue-grid/docs/guides/filtering.md b/packages/dx-vue-grid/docs/guides/filtering.md index 737bcc67d6..4bc6c2565f 100644 --- a/packages/dx-vue-grid/docs/guides/filtering.md +++ b/packages/dx-vue-grid/docs/guides/filtering.md @@ -32,21 +32,27 @@ You can prevent filtering by a specific column using the [DxFilteringState](../r .embedded-demo({ "path": "grid-filtering/disable-column-filtering", "showThemeSelector": true }) +## Custom Filter Operations + +Specify the [DxTableFilterRow](../reference/table-filter-row.md) plugin's `showFilterSelector` property to allow filter operation selection for an end user. Define which filter operations are available for particular columns using the [DxDataTypeProvider](../reference/data-type-provider.md) `availableFilterOperations` property. You can also define a custom operation by passing a filtering predicate to the [DxIntegratedFiltering](../reference/integrated-filtering.md) plugin's `columnExtensions` property. + +.embedded-demo({ "path": "grid-filtering/advanced-filter-row", "showThemeSelector": true }) + ## Customizing Filter Row Appearance Pass a function that returns a custom component to the `DxTableFilterRow` plugin's `cellComponent` property to substitute the built-in filter row editors. In this case, delegate the component's state management to the `DxTableFilterRow` plugin by assigning the function's `filter` and `onFilter` arguments to the appropriate component's properties. .embedded-demo({ "path": "grid-filtering/custom-filter-row", "showThemeSelector": true }) - +.embedded-demo({ "path": "grid-filtering/remote-filtering", "showThemeSelector": true }) ## Using Filtering with Other Data Processing Plugins diff --git a/packages/dx-vue-grid/docs/reference/integrated-filtering.md b/packages/dx-vue-grid/docs/reference/integrated-filtering.md index 2cdb2d83c8..168f50dd0c 100644 --- a/packages/dx-vue-grid/docs/reference/integrated-filtering.md +++ b/packages/dx-vue-grid/docs/reference/integrated-filtering.md @@ -43,6 +43,12 @@ Field | Type | Description operator | 'and' | 'or' | Specifies the Boolean operator filters | Array<[FilterExpression](#filterexpression) | [Filter](filtering-state.md#filter)> | Specifies filters or filter expressions +## Static Fields + +Field | Type | Description +------|------|------------ +defaultPredicate | (value: any, filter: [Filter](filtering-state.md#filter), row: any) => boolean | The built-in filter predicate. The `filter` parameter accepts an object containing the 'value' field. + ## Plugin Developer Reference ### Imports diff --git a/packages/dx-vue-grid/docs/reference/table-filter-row.md b/packages/dx-vue-grid/docs/reference/table-filter-row.md index 54a6f76c39..1727fd102d 100644 --- a/packages/dx-vue-grid/docs/reference/table-filter-row.md +++ b/packages/dx-vue-grid/docs/reference/table-filter-row.md @@ -30,6 +30,10 @@ Name | Type | Default | Description -----|------|---------|------------ cellComponent | [DxTableFilterRow.DxCell](#dxtablefilterrowdxcell) | | A component that renders a filter cell. rowComponent | [DxTableFilterRow.DxRow](#dxtablefilterrowdxrow) | | A component that renders a filter row. +filterSelectorComponent | [DxTableFilterRow.DxFilterSelector](#dxtablefilterrowdxfilterselector) | | A component that renders a filter selector. +iconComponent | [DxTableFilterRow.DxIcon](#dxtablefilterrowdxicon) | | A component that renders filter selector icons. +editorComponent | [DxTableFilterRow.DxEditor](#dxtablefilterrowdxeditor) | | A component that renders a filter editor. +showFilterSelector? | boolean | false | Specifies whether the FilterSelector should be displayed. rowHeight? | number | | The filter row's height. messages? | [DxTableFilterRow.LocalizationMessages](#localization-messages) | | An object that specifies localization messages. @@ -76,11 +80,63 @@ Field | Description ------|------------ default | The default Vue slot. +### DxTableFilterRow.DxFilterSelector + +#### Props + +Field | Type | Description +------|------|------------ +iconComponent | [DxTableFilterRow.DxIcon](#dxtablefilterrowdxicon) | A component that renders filter selector icons. +value | string | The currently selected filter operation. +availableValues | Array | The list of available filter operations. +disabled | boolean | Specifies whether the FilterSelector is disabled. +getMessage | (messageKey: string) => string | Returns the specified localization message. + +#### Events + +Field | Type | Description +------|------|------------ +changeValue | (value: string) => void | Handles filter operation changes. + +### DxTableFilterRow.DxIcon + +#### Props + +Field | Type | Description +------|------|------------ +type | string | Specifies the icon type. + +### DxTableFilterRow.DxEditor + +#### Props + +Field | Type | Description +------|------|------------ +value | any | The current editor value. +disabled | boolean | Specifies whether the editor is disabled. +getMessage | (messageKey: string) => string | Returns the specified localization message. + +#### Events + +Field | Type | Description +------|------|------------ +changeValue | (value: string) => void | Handles filter value changes. + ## Localization Messages Field | Type | Default | Description ------|------|---------|------------ filterPlaceholder? | string | 'Filter...' | The filter editor placeholder text. +contains? | string | 'Contains' | The 'contains' filter operation name. +notContains? | string | 'Does not contain'| The 'notContains' filter operation name. +startsWith? | string | 'Starts with'| The 'startsWith' filter operation name. +endsWith? | string | 'Ends with'| The 'endsWith' filter operation name. +equal? | string | 'Equals'| The 'equal' filter operation name. +notEqual? | string | 'Does not equal'| The 'notEqual' filter operation name. +greaterThan? | string | 'Greater than'| The 'greaterThan' filter operation name. +greaterThanOrEqual? | string | 'Greater than or equal to'| The 'greaterThanOrEqual' filter operation name. +lessThan? | string | 'Less than' | The 'lessThan' filter operation name. +lessThanOrEqual? | string | 'Less than or equal to' | The 'lessThanOrEqual' filter operation name. ## Plugin Components @@ -88,6 +144,9 @@ Name | Type | Description -----|------|------------ DxTableFilterRow.components.DxCell | [DxTableFilterRow.DxCell](#dxtablefilterrowdxcell) | A component that renders a filter cell. DxTableFilterRow.components.DxRow | [DxTableFilterRow.DxRow](#dxtablefilterrowdxrow) | A component that renders a filter row. +DxTableFilterRow.components.DxFilterSelector | [DxTableFilterRow.DxFilterSelector](#dxtablefilterrowdxfilterselector) | A component that renders a filter selector. +DxTableFilterRow.components.DxIcon | [DxTableFilterRow.DxIcon](#dxtablefilterrowdxicon) | A component that renders filter selector icons. +DxTableFilterRow.components.DxEditor | [DxTableFilterRow.DxEditor](#dxtablefilterrowdxeditor) | A component that renders a filter editor. ## Plugin Developer Reference @@ -102,6 +161,7 @@ changeColumnFilter | Action | ({ columnName: string, config: object }) => void | tableCell | Template | object? | A template that renders a table cell. tableRow | Template | object? | A template that renders a table row. valueEditor | Template | [DxDataTypeProvider.DxValueEditor](data-type-provider.md#dxdatatypeproviderdxvalueeditor) | A template that renders the editor. +getAvailableFilterOperations | Getter | (columnName: string) => Array<string>? | A function that returns the names of filter operations that are available for a particular column. ### Exports diff --git a/packages/dx-vue-grid/src/plugins/column-chooser.js b/packages/dx-vue-grid/src/plugins/column-chooser.js index f4d4e6cbdc..814be39f2c 100644 --- a/packages/dx-vue-grid/src/plugins/column-chooser.js +++ b/packages/dx-vue-grid/src/plugins/column-chooser.js @@ -74,7 +74,7 @@ export const DxColumnChooser = { toggleColumnVisibility, }, }) => ( -
+
+ getAvailableFilterOperationsGetter( + getAvailableFilterOperations, + availableFilterOperations, + columnNames, + ); + return ( + {Formatter ? ( isFilterTableCell(tableRow, tableColumn)} > - {({ attrs, listerens }) => ( + {({ attrs, listeners }) => ( {({ - getters: { filters, isColumnFilteringEnabled }, + getters: { filters, isColumnFilteringEnabled, getAvailableFilterOperations }, actions: { changeColumnFilter }, }) => { const { name: columnName } = attrs.tableColumn.column; const filter = getColumnFilterConfig(filters, columnName); - const onFilter = (config) => { - changeColumnFilter({ columnName, config }); + const onFilter = config => changeColumnFilter({ columnName, config }); + const columnFilterOperations = + getColumnFilterOperations(getAvailableFilterOperations, columnName); + const selectedFilterOperation = filterOperations[columnName] + || columnFilterOperations[0]; + const handleFilterOperationChange = (value) => { + this.filterOperations = { + ...filterOperations, + [columnName]: value, + }; + if (filter && !isFilterValueEmpty(filter.value)) { + onFilter({ value: filter.value, operation: value }); + } + }; + const handleFilterValueChange = (value) => { + onFilter(!isFilterValueEmpty(value) + ? { value, operation: selectedFilterOperation } + : null); }; + const filteringEnabled = isColumnFilteringEnabled(columnName); return ( - + value={filter ? filter.value : undefined} + onValueChange={handleFilterValueChange} + > + {content => ( + + {showFilterSelector + ? ( + + ) : null + } + {content || ( + + )} + + )} + ); }} @@ -84,9 +159,9 @@ export const DxTableFilterRow = { name="tableRow" predicate={({ attrs: { tableRow } }) => isFilterTableRow(tableRow)} > - {({ attrs, listerens, slots }) => + {({ attrs, listeners, slots }) => {slots.default} diff --git a/packages/dx-vue-grid/src/plugins/table-filter-row.test.js b/packages/dx-vue-grid/src/plugins/table-filter-row.test.js index 5d7afc8668..cd048ee628 100644 --- a/packages/dx-vue-grid/src/plugins/table-filter-row.test.js +++ b/packages/dx-vue-grid/src/plugins/table-filter-row.test.js @@ -6,6 +6,9 @@ import { isFilterTableCell, isFilterTableRow, getMessagesFormatter, + getColumnFilterOperations, + isFilterValueEmpty, + getColumnFilterConfig, } from '@devexpress/dx-grid-core'; import { DxTableFilterRow } from './table-filter-row'; import { PluginDepsToComponents, getComputedState } from './test-utils'; @@ -16,6 +19,8 @@ jest.mock('@devexpress/dx-grid-core', () => ({ isFilterTableRow: jest.fn(), getColumnFilterConfig: jest.fn(), getMessagesFormatter: jest.fn(), + getColumnFilterOperations: jest.fn(), + isFilterValueEmpty: jest.fn(), })); const defaultDeps = { @@ -42,8 +47,11 @@ const defaultDeps = { }; const defaultProps = { - cellComponent: { name: 'Cell', render() { return null; } }, + cellComponent: { name: 'Cell', render() { return
{this.$slots.default}
; } }, rowComponent: { name: 'Row', render() { return null; } }, + iconComponent: { name: 'Icon', render() { return null; } }, + editorComponent: { name: 'Editor', render() { return null; } }, + filterSelectorComponent: { name: 'FilterSelector', render() { return null; } }, }; describe('DxTableFilterRow', () => { @@ -57,9 +65,11 @@ describe('DxTableFilterRow', () => { beforeEach(() => { tableHeaderRowsWithFilter.mockImplementation(() => 'tableHeaderRowsWithFilter'); - isFilterTableCell.mockImplementation(() => false); + isFilterTableCell.mockImplementation(() => true); isFilterTableRow.mockImplementation(() => false); + getColumnFilterOperations.mockImplementation(() => []); getMessagesFormatter.mockImplementation(messages => key => (messages[key] || key)); + isFilterValueEmpty.mockImplementation(() => false); }); afterEach(() => { jest.resetAllMocks(); @@ -160,4 +170,167 @@ describe('DxTableFilterRow', () => { const { getMessage } = tree.find(defaultProps.cellComponent).vm.$attrs; expect(getMessage('filterPlaceholder')).toBe('Filter...'); }); + + it('should render a cell with a disabled filtering editor if filtering is not allowed for the column', () => { + const deps = { + getter: { + isColumnFilteringEnabled: () => false, + }, + }; + const tree = mount({ + render() { + return ( + + + + + ); + }, + }); + + expect(tree.find(defaultProps.editorComponent).vm.$attrs.disabled) + .toBeTruthy(); + }); + + it('should change filter correctly on editor value change', () => { + const tree = mount({ + render() { + return ( + + + + + ); + }, + }); + tree.find(defaultProps.editorComponent) + .vm.$listeners.changeValue('a'); + + expect(defaultDeps.action.changeColumnFilter.mock.calls[0][0]) + .toMatchObject({ config: { value: 'a' } }); + }); + + it('should reset the filter when an empty value is set', () => { + isFilterValueEmpty.mockImplementation(() => true); + const tree = mount({ + render() { + return ( + + + + + ); + }, + }); + tree.find(defaultProps.editorComponent) + .vm.$listeners.changeValue({ target: {} }); + + expect(defaultDeps.action.changeColumnFilter.mock.calls[0][0]) + .toMatchObject({ config: null }); + }); + + it('can render filter selector', () => { + let tree = mount({ + render() { + return ( + + + + + ); + }, + }); + + expect(tree.find(defaultProps.filterSelectorComponent).exists()) + .toBeFalsy(); + + tree = mount({ + render() { + return ( + + + + + ); + }, + }); + + expect(tree.find(defaultProps.filterSelectorComponent).exists()) + .toBeTruthy(); + }); + + it('should change filter correctly on filter operation change', () => { + getColumnFilterConfig.mockImplementation(() => ({ value: 1 })); + const tree = mount({ + render() { + return ( + + + + + ); + }, + }); + tree.find(defaultProps.filterSelectorComponent) + .vm.$listeners.changeValue('a'); + + expect(defaultDeps.action.changeColumnFilter.mock.calls[0][0]) + .toMatchObject({ config: { operation: 'a' } }); + }); + + it('should not change filter on filter operation change if filter value is empty', () => { + isFilterValueEmpty.mockImplementation(() => true); + const tree = mount({ + render() { + return ( + + + + + ); + }, + }); + tree.find(defaultProps.filterSelectorComponent) + .vm.$listeners.changeValue('a'); + + expect(defaultDeps.action.changeColumnFilter) + .not.toHaveBeenCalled(); + }); + + it('should use the first available operation as the FilterSelector value by default', () => { + getColumnFilterOperations.mockImplementation(() => ['a', 'b', 'c']); + const tree = mount({ + render() { + return ( + + + + + ); + }, + }); + + expect(tree.find(defaultProps.filterSelectorComponent).vm.$attrs.value) + .toBe('a'); + }); });