diff --git a/CHANGELOG.md b/CHANGELOG.md index a11dea522..0a59ac368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ column filters not working. ## [4.4.0] - 2019-10-08 ### Added +[#545](https://github.com/plotly/dash-table/issues/545) +- Case insensitive filtering +- New props: `filter_case` - to control case of all filters, `columns.filter_case` - to control filter case for each column +- New operators: `i=`, `i>=`, `i>`, `i<=`, `i<`, `i!=`, `icontains` - for case-insensitive filtering, `s=`, `s>=`, `s>`, `s<=`, `s<`, `s!=`, `scontains` - to force case-sensitive filtering on case-insensitive columns + [#546](https://github.com/plotly/dash-table/issues/546) - New prop `export_columns` that takes values `all` or `visible` (default). This prop controls the columns used during export diff --git a/demo/App.tsx b/demo/App.tsx index 6a7ce3121..2e9c76166 100644 --- a/demo/App.tsx +++ b/demo/App.tsx @@ -8,6 +8,8 @@ import Logger from 'core/Logger'; import AppState, { AppMode, AppFlavor } from './AppMode'; import './style.less'; +import FilterCaseButton from 'dash-table/components/Filter/FilterCaseButton'; +import { Case } from 'dash-table/components/Table/props'; class App extends Component { constructor(props: any) { @@ -25,7 +27,7 @@ class App extends Component { const flavors = flavorParam ? flavorParam.split(';') : []; if (flavors.indexOf(AppFlavor.FilterNative) !== -1) { - return (
+ return (
+ { + const tableProps = R.clone(this.state.tableProps); + tableProps.filter_case = tableProps.filter_case === Case.Insensitive + ? Case.Sensitive : Case.Insensitive; + + this.setState({ tableProps }); + }} + /> { this.setState({ tableProps }); }} /> +
); } else if (mode === AppMode.TaleOfTwoTables) { if (!this.state.tableProps2) { @@ -78,7 +92,7 @@ class App extends Component { return (newProps: any) => { Logger.debug('--->', newProps); this.setState((prevState: any) => ({ - tableProps: R.merge(prevState.tableProps, newProps) + tableProps: R.mergeRight(prevState.tableProps, newProps) })); }; }); diff --git a/demo/AppMode.ts b/demo/AppMode.ts index bf92099ed..599ef14dd 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -54,7 +54,7 @@ export const BasicModes = [ function getBaseTableProps(mock: IDataMock): Partial { return { id: 'table', - columns: mock.columns.map((col: any) => R.merge(col, { + columns: mock.columns.map((col: any) => R.mergeRight(col, { name: col.name || col.id, on_change: { action: ChangeAction.None @@ -108,7 +108,7 @@ function getDefaultState( return { filter_query: '', - tableProps: R.merge(getBaseTableProps(mock), { + tableProps: R.mergeRight(getBaseTableProps(mock), { data: mock.data, editable: true, sort_action: TableAction.Native, @@ -271,7 +271,7 @@ function getVirtualizedState() { return { filter_query: '', - tableProps: R.merge(getBaseTableProps(mock), { + tableProps: R.mergeRight(getBaseTableProps(mock), { data: mock.data, editable: true, fill_width: false, diff --git a/demo/style.less b/demo/style.less index f98f17c54..d7c03c948 100644 --- a/demo/style.less +++ b/demo/style.less @@ -1,3 +1,16 @@ html { font-size: 13px; -} + + .demo-app-root { + input.dash-filter--case { + outline: none; + height: 18px; + } + + input.dash-filter--case--sensitive { + border-color: hotpink; + border-radius: 4px; + border-width: 2px; + } + } +} \ No newline at end of file diff --git a/src/core/syntax-tree/index.ts b/src/core/syntax-tree/index.ts index c689ddc8a..d769ff4f9 100644 --- a/src/core/syntax-tree/index.ts +++ b/src/core/syntax-tree/index.ts @@ -9,6 +9,7 @@ interface IStructure { subType?: string; type: string; value: any; + case: string | undefined; block?: IStructure; left?: IStructure; @@ -21,7 +22,8 @@ function toStructure(tree: ISyntaxTree): IStructure { const res: IStructure = { subType: lexeme.subType, type: lexeme.type, - value: lexeme.present ? lexeme.present(tree) : value + value: lexeme.present ? lexeme.present(tree) : value, + case: lexeme.case }; if (block) { diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index e0eb5b473..671cbc67b 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -16,6 +16,7 @@ export interface IUnboundedLexeme { resolve?: (target: any, tree: ISyntaxTree) => any; subType?: string; type: string; + case?: string; nesting?: number; priority?: number; regexp: RegExp; diff --git a/src/dash-table/components/CellFactory.tsx b/src/dash-table/components/CellFactory.tsx index bafb906fd..024aabada 100644 --- a/src/dash-table/components/CellFactory.tsx +++ b/src/dash-table/components/CellFactory.tsx @@ -210,7 +210,7 @@ export default class CellFactory { ) => { return React.cloneElement(wrapper, { children: [content], - style: R.merge(style, { borderBottom, borderLeft, borderRight, borderTop }) + style: R.mergeRight(style, { borderBottom, borderLeft, borderRight, borderTop }) }); }); diff --git a/src/dash-table/components/Filter/Column.tsx b/src/dash-table/components/Filter/Column.tsx index 62084d450..7433f7eeb 100644 --- a/src/dash-table/components/Filter/Column.tsx +++ b/src/dash-table/components/Filter/Column.tsx @@ -1,54 +1,78 @@ +import * as R from 'ramda'; import React, { CSSProperties, PureComponent } from 'react'; import IsolatedInput from 'core/components/IsolatedInput'; +import FilterCaseButton from './FilterCaseButton'; -import { ColumnId } from 'dash-table/components/Table/props'; +import { Case, SetProps, IColumn } from 'dash-table/components/Table/props'; import TableClipboardHelper from 'dash-table/utils/TableClipboardHelper'; -type SetFilter = (ev: any) => void; +type SetFilter = (ev: any, c: Case, column: IColumn) => void; interface IColumnFilterProps { classes: string; - columnId: ColumnId; + column: IColumn; + columns: IColumn[]; isValid: boolean; setFilter: SetFilter; + setProps: SetProps; style?: CSSProperties; value?: string; + globalFilterCase: Case; + columnFilterCase: Case; } -interface IState { - value?: string; -} - -export default class ColumnFilter extends PureComponent { - constructor(props: IColumnFilterProps) { - super(props); +export default class ColumnFilter extends PureComponent { + private submit = (value: string | undefined) => { + const { column, setFilter, columnFilterCase } = this.props; - this.state = { - value: props.value - }; + setFilter( + column, + this.getComputedCase(columnFilterCase), + value as any); } - private submit = (value: string | undefined) => { - const { setFilter } = this.props; + private setColumnCase = () => { + const { columns, column, setFilter, columnFilterCase, globalFilterCase, setProps, value } = this.props; - setFilter({ - target: { value } - } as any); + const cols: IColumn[] = R.clone(columns); + const inx: number = R.findIndex(R.propEq('id', column.id))(cols); + + const newColumnFilterCase = globalFilterCase === Case.Sensitive + ? ((columnFilterCase === Case.Sensitive || columnFilterCase === Case.Default) + ? Case.Insensitive : Case.Default) + : ((columnFilterCase === Case.Insensitive || columnFilterCase === Case.Default) + ? Case.Sensitive : Case.Default); + + const newComputedCase = this.getComputedCase(newColumnFilterCase); + + cols[inx].filter_case = newColumnFilterCase; + + setFilter( + cols[inx], + newComputedCase, + value || '' as any); + setProps({ columns: cols }); } + private getComputedCase = (columnFilterCase: Case) => + (columnFilterCase === Case.Insensitive || + (this.props.globalFilterCase === Case.Insensitive && columnFilterCase !== Case.Sensitive)) + ? Case.Insensitive : Case.Sensitive + render() { const { classes, - columnId, + column, isValid, style, - value + value, + columnFilterCase } = this.props; return ( +
+ +
); } } \ No newline at end of file diff --git a/src/dash-table/components/Filter/FilterCaseButton.tsx b/src/dash-table/components/Filter/FilterCaseButton.tsx new file mode 100644 index 000000000..5f647984b --- /dev/null +++ b/src/dash-table/components/Filter/FilterCaseButton.tsx @@ -0,0 +1,22 @@ +import React, { PureComponent } from 'react'; + +import { Case } from 'dash-table/components/Table/props'; + +interface IFilterCaseButtonProps { + filterCase: Case; + setColumnCase: () => void; +} + +export default class FilterCaseButton extends PureComponent { + render() { + const filterCaseClass: string = (this.props.filterCase !== Case.Insensitive) ? + 'dash-filter--case--sensitive' : 'dash-filter--case--insensitive'; + + return (); + } +} \ No newline at end of file diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index a56673afa..0db9fa5e8 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -7,7 +7,7 @@ import memoizerCache from 'core/cache/memoizer'; import { memoizeOne } from 'core/memoizer'; import ColumnFilter from 'dash-table/components/Filter/Column'; -import { ColumnId, IColumn, TableAction, IFilterFactoryProps, SetFilter } from 'dash-table/components/Table/props'; +import { ColumnId, IColumn, TableAction, IFilterFactoryProps, SetFilter, Case, SetProps } from 'dash-table/components/Table/props'; import derivedFilterStyles, { derivedFilterOpStyles } from 'dash-table/derived/filter/wrapperStyles'; import derivedHeaderOperations from 'dash-table/derived/header/operations'; import { derivedRelevantFilterStyles } from 'dash-table/derived/style'; @@ -32,29 +32,37 @@ export default class FilterFactory { } - private onChange = (column: IColumn, map: Map, setFilter: SetFilter, ev: any) => { - Logger.debug('Filter -- onChange', column.id, ev.target.value && ev.target.value.trim()); + private onChange = + (map: Map, setFilter: SetFilter, column: IColumn, computed_filter_case: Case, ev: any) => { + Logger.debug('Filter -- onChange', column.id, ev && ev.trim()); - const value = ev.target.value.trim(); + const value = ev && ev.trim(); - updateColumnFilter(map, column, value, setFilter); + updateColumnFilter(map, column, value, setFilter, computed_filter_case); } private filter = memoizerCache<[ColumnId, number]>()(( column: IColumn, + columns: IColumn[], index: number, map: Map, - setFilter: SetFilter + setFilter: SetFilter, + setProps: SetProps, + filter_case: Case ) => { const ast = map.get(column.id.toString()); return (); }); @@ -63,7 +71,7 @@ export default class FilterFactory { edges: IEdgesMatrices | undefined ) => arrayMap( styles, - (s, j) => R.merge( + (s, j) => R.mergeRight( s, edges && edges.getStyle(0, j) ) @@ -74,16 +82,19 @@ export default class FilterFactory { filterOpEdges: IEdgesMatrices | undefined ) { const { + columns, filter_action, map, row_deletable, row_selectable, setFilter, + setProps, style_cell, style_cell_conditional, style_filter, style_filter_conditional, - visibleColumns + visibleColumns, + filter_case } = this.props; if (filter_action === TableAction.None) { @@ -111,9 +122,12 @@ export default class FilterFactory { const filters = R.addIndex(R.map)((column, index) => { return this.filter.get(column.id, index)( column, + columns, index, map, - setFilter + setFilter, + setProps, + filter_case ); }, visibleColumns); diff --git a/src/dash-table/components/HeaderFactory.tsx b/src/dash-table/components/HeaderFactory.tsx index be8f03eeb..9cdcc5a87 100644 --- a/src/dash-table/components/HeaderFactory.tsx +++ b/src/dash-table/components/HeaderFactory.tsx @@ -56,7 +56,8 @@ export default class HeaderFactory { style_cell_conditional, style_header, style_header_conditional, - visibleColumns + visibleColumns, + filter_case } = props; const labelsAndIndices = this.labelsAndIndices(columns, visibleColumns, merge_duplicate_headers); @@ -109,7 +110,8 @@ export default class HeaderFactory { page_action, setFilter, setProps, - merge_duplicate_headers + merge_duplicate_headers, + filter_case ); const ops = this.getHeaderOpCells( diff --git a/src/dash-table/components/Table/Table.less b/src/dash-table/components/Table/Table.less index fe3422494..d96567413 100644 --- a/src/dash-table/components/Table/Table.less +++ b/src/dash-table/components/Table/Table.less @@ -143,82 +143,80 @@ .dash-table-container { .previous-next-container { - display: inline-block; - float: right; - padding: 5px 0px; - - - .page-number { - font-family: monospace; - display: inline-block; - .last-page { - min-width: 30px; - display: inline-block; - text-align: center; - margin: 0.5em; - } - } - - input.current-page { - display: inline-block; - border-bottom: solid lightgrey 1px !important; - color: black; - border: none; - width: 30px; - text-align: center; - font-family: monospace; - font-size: 10pt; - margin: 0.5em; - - &::placeholder { - color: black; - } - - &:focus { - outline: none; - - &::placeholder { - opacity: 0; - } - } - } - - button.previous-page, button.next-page, button.first-page, button.last-page { - transition-duration: 400ms; - padding: 5px; - border: none; - display: inline-block; - margin-left: 5px; - margin-right: 5px; - - &:hover { - color: hotpink; - - &:disabled { - color: graytext - } - } + float: right; + padding: 5px 0px; - &:focus { - outline: none; - } - } + .page-number { + font-family: monospace; + display: inline-block; + .last-page { + min-width: 30px; + display: inline-block; + text-align: center; + margin: 0.5em; + } + } + + input.current-page { + display: inline-block; + border-bottom: solid lightgrey 1px !important; + color: black; + border: none; + width: 30px; + text-align: center; + font-family: monospace; + font-size: 10pt; + margin: 0.5em; + + &::placeholder { + color: black; + } + + &:focus { + outline: none; + + &::placeholder { + opacity: 0; + } + } + } + + button.previous-page, button.next-page, button.first-page, button.last-page { + transition-duration: 400ms; + padding: 5px; + border: none; + display: inline-block; + margin-left: 5px; + margin-right: 5px; + + &:hover { + color: hotpink; + + &:disabled { + color: graytext + } + } + + &:focus { + outline: none; + } + } } .dash-spreadsheet-container { - .reset-css(); - display: flex; - flex-direction: row; - position: relative; - - // This overrides Chrome's default `font-size: medium;` which is causing performance issues - // with AutoInputResize sub-component in react-select - // https://github.com/JedWatson/react-input-autosize/blob/05b0f86a7f8b16de99c2b31296ff0d3307f15957/src/AutosizeInput.js#L58 - table { + .reset-css(); + display: flex; + flex-direction: row; + position: relative; + + // This overrides Chrome's default `font-size: medium;` which is causing performance issues + // with AutoInputResize sub-component in react-select + // https://github.com/JedWatson/react-input-autosize/blob/05b0f86a7f8b16de99c2b31296ff0d3307f15957/src/AutosizeInput.js#L58 + table { font-size: inherit; - } + } - .dash-spreadsheet-inner { + .dash-spreadsheet-inner { box-sizing: border-box; display: flex; flex-direction: column; @@ -226,230 +224,245 @@ *, *:after, *:before { - box-sizing: inherit; + box-sizing: inherit; } .Select { - overflow: hidden; - position: static; + overflow: hidden; + position: static; } .Select, .Select-control { - background-color: inherit; + background-color: inherit; } .Select-value { - display: flex; - flex-direction: column; - justify-content: center; - margin-top: -2px; + display: flex; + flex-direction: column; + justify-content: center; + margin-top: -2px; } .marker-row { - tr { + tr { visibility: hidden !important; - } + } - td, th { + td, th { height: 0 !important; padding: 0 !important; margin: 0 !important; - } + } } .dash-filter { - input::placeholder { + input::placeholder { color: inherit; font-size: 0.8em; padding-right: 5px; - } + } - & + .dash-filter { + & + .dash-filter { &:not(:hover):not(:focus-within) { - input::placeholder { + input::placeholder { color: transparent; - } + } } - } + } - &.invalid { + &.invalid { background-color: pink; - } + } } &:not(.dash-empty-11) { - .row-0 { + .row-0 { tr:last-of-type { - td, th { + td, th { border-bottom: none !important; - } + } } - } + } } &:not(.dash-empty-01) { - .cell-0-0, - .cell-1-0 { + .cell-0-0, + .cell-1-0 { tr { - td:last-of-type, - th:last-of-type { + td:last-of-type, + th:last-of-type { border-right: none !important; - } + } } - } + } } &.dash-freeze-left, &.dash-freeze-top, &.dash-virtualized { - overflow: hidden !important; + overflow: hidden !important; - .row-0 { + .row-0 { display: flex; flex: 0 0 auto; flex-direction: row; - } + } - .row-1 { + .row-1 { display: flex; flex-direction: row; overflow: auto; - } + } - .cell-0-0, - .cell-1-0 { + .cell-0-0, + .cell-1-0 { flex: 0 0 auto; left: 0; position: sticky; position:-webkit-sticky; z-index: 400; - } + } - .cell-0-1 { + .cell-0-1 { z-index: 300; flex: 0 0 auto; - } + } - .cell-1-1 { + .cell-1-1 { flex: 0 0 auto; - } + } } &.dash-fill-width { - .cell-0-1, - .cell-1-1 { + .cell-0-1, + .cell-1-1 { flex: 1 0 auto; - } + } - table { + table { width: 100%; - } + } } td { - background-color: inherit; + background-color: inherit; - &.focused { + &.focused { margin: -1px; z-index: 200; - } + } - .dash-cell-value-container { + .dash-cell-value-container { width: 100%; height: 100%; - } + } - .dash-input-cell-value-container { + .dash-input-cell-value-container { position: relative; - } + } - .dash-cell-value { + .dash-cell-value { height: 100%; width: 100%; - } + } - input.dash-cell-value { + input.dash-cell-value { position: absolute; left: 0; top: 0; &.unfocused::selection { - background-color: transparent; + background-color: transparent; } &.unfocused { - caret-color: transparent; + caret-color: transparent; } - } + } - .cell-value-shadow { + .cell-value-shadow { margin: auto 0; opacity: 0; - } + } - .input-cell-value-shadow { + .input-cell-value-shadow { display: inline-block; height: initial; width: initial; - } + } - .dropdown-cell-value-shadow { + .dropdown-cell-value-shadow { display: block; height: 0px; padding: 0 42px 0 10px; - } + } } th.dash-filter { - position: relative; + position: relative; - & input { + & input { position: absolute; left: 0; top: 0; height: 100%; width: 100%; - } + &.dash-filter--case { + position: relative; + left: auto; + top: auto; + width: auto; + height: 16px; + line-height: 0px; + padding: 1px; + } + &.dash-filter--case--sensitive { + border-width: 1px; + border-style: solid; + border-radius: 3px; + } + } } th { - white-space: nowrap; - - .column-header--clear, - .column-header--delete, - .column-header--edit, - .column-header--hide - .column-header--sort { + white-space: nowrap; + + .dash-filter--case, + .column-header--clear, + .column-header--delete, + .column-header--edit, + .column-header--hide + .column-header--sort { .not-selectable(); cursor: pointer; - } + } } // cell content styling td, th { - background-clip: padding-box; - padding: 2px; - overflow-x: hidden; - white-space: nowrap; + background-clip: padding-box; + padding: 2px; + overflow-x: hidden; + white-space: nowrap; - height: 30px; + height: 30px; - text-align: right; + text-align: right; - div.dash-cell-value { + div.dash-cell-value { display: inline; vertical-align: middle; white-space: inherit; overflow: inherit; text-overflow: inherit; - } + } } - } + } - .dash-spreadsheet-inner textarea { + .dash-spreadsheet-inner textarea { white-space: pre; - } + } - .dash-spreadsheet-inner table { + .dash-spreadsheet-inner table { border-collapse: collapse; font-family: monospace; @@ -463,24 +476,24 @@ --selected-background: rgba(255, 65, 54, 0.2); --faded-dropdown: rgb(240, 240, 240); --muted: rgb(200, 200, 200); - } + } - /* focus happens after copying to clipboard */ - .dash-spreadsheet-inner table:focus { + /* focus happens after copying to clipboard */ + .dash-spreadsheet-inner table:focus { outline: none; - } + } - .dash-spreadsheet-inner thead { + .dash-spreadsheet-inner thead { display: table-row-group; - } + } - .elip { + .elip { text-align: center; width: 100%; background-color: var(--background-color-ellipses); - } + } - .dash-spreadsheet-inner td.dropdown { + .dash-spreadsheet-inner td.dropdown { /* * To view the dropdown's contents, we need * overflow-y: visible. @@ -494,30 +507,30 @@ * tried it. */ overflow-x: visible; - } + } - .dash-spreadsheet-inner :not(.cell--selected) tr:hover, - tr:hover input :not(.cell--selected) { + .dash-spreadsheet-inner :not(.cell--selected) tr:hover, + tr:hover input :not(.cell--selected) { background-color: var(--hover); - } + } - .dash-spreadsheet-inner th { + .dash-spreadsheet-inner th { background-color: rgb(250, 250, 250); - } + } - .dash-spreadsheet-inner td { + .dash-spreadsheet-inner td { background-color: white; - } + } - .expanded-row--empty-cell { + .expanded-row--empty-cell { background-color: transparent; - } + } - .expanded-row { + .expanded-row { text-align: center; - } + } - .dash-spreadsheet-inner input:not([type=radio]):not([type=checkbox]) { + .dash-spreadsheet-inner input:not([type=radio]):not([type=checkbox]) { padding: 0px; margin: 0px; height: calc(100% - 1px); @@ -534,19 +547,19 @@ * or bare `td` */ text-shadow: none; - } + } - .dash-spreadsheet-inner input.unfocused { + .dash-spreadsheet-inner input.unfocused { color: transparent; text-shadow: 0 0 0 var(--text-color); cursor: default; - } + } - .dash-spreadsheet-inner input.unfocused:focus { + .dash-spreadsheet-inner input.unfocused:focus { outline: none; - } + } - .toggle-row { + .toggle-row { border: none; box-shadow: none; width: 10px; @@ -554,61 +567,76 @@ padding-right: 10px; cursor: pointer; color: var(--faded-text); - } + } - .toggle-row--expanded { + .toggle-row--expanded { color: var(--accent); - } + } - .dash-spreadsheet-inner tr:hover .toggle-row { + .dash-spreadsheet-inner tr:hover .toggle-row { color: var(--accent); - } + } - .dash-spreadsheet-inner .dash-delete-cell, - .dash-spreadsheet-inner .dash-delete-header { + .dash-spreadsheet-inner .dash-delete-cell, + .dash-spreadsheet-inner .dash-delete-header { .not-selectable(); font-size: 1.3rem; text-align: center; cursor: pointer; color: var(--muted); - } - .dash-spreadsheet-inner .dash-delete-cell:hover, - .dash-spreadsheet-inner .dash-delete-header:hover { + } + .dash-spreadsheet-inner .dash-delete-cell:hover, + .dash-spreadsheet-inner .dash-delete-header:hover { color: var(--accent); - } + } - .dash-spreadsheet-inner { - [class^='column-header--'] { - cursor: pointer; - float: left; + .dash-spreadsheet-inner { + [class^='column-header--'],[class^='dash-filter--'] { + cursor: pointer; + float: left; + } + + .dash-filter--case { + font-weight: 900; } + .dash-filter--case, .column-header--select { - height: auto; + height: auto; } .column-header--select, .column-header--sort { - color: var(--faded-text-header); + color: var(--faded-text-header); } - + .dash-filter--case, .column-header--clear, .column-header--delete, .column-header--edit, .column-header--hide { - opacity: 0.1; - padding-left: 2px; - padding-right: 2px; + opacity: 0.1; + padding-left: 2px; + padding-right: 2px; } th:hover { - [class^='column-header--']:not(.disabled) { + [class^='column-header--'],[class^='dash-filter--']:not(.disabled) { color: var(--accent); opacity: 1; - } + } + } + + .dash-filter--case { + font-size: 10px; } - } + + .dash-filter--case--sensitive { + border-width: 1px; + border-style: solid; + border-radius: 3px; + } + } } -} +} \ No newline at end of file diff --git a/src/dash-table/components/Table/controlledPropsHelper.ts b/src/dash-table/components/Table/controlledPropsHelper.ts index 806777e82..096fd0338 100644 --- a/src/dash-table/components/Table/controlledPropsHelper.ts +++ b/src/dash-table/components/Table/controlledPropsHelper.ts @@ -47,7 +47,7 @@ export default () => { uiViewport, virtualization, visibleColumns - } = R.merge(props, state) as (SanitizedAndDerivedProps & StandaloneState); + } = R.mergeRight(props, state) as (SanitizedAndDerivedProps & StandaloneState); const virtual = getVirtual( visibleColumns, diff --git a/src/dash-table/components/Table/index.tsx b/src/dash-table/components/Table/index.tsx index f20893e7a..f371829bc 100644 --- a/src/dash-table/components/Table/index.tsx +++ b/src/dash-table/components/Table/index.tsx @@ -40,7 +40,8 @@ export default class Table extends Component(), props.filter_query, - props.visibleColumns + props.visibleColumns, + props.filter_case ) }, rawFilterQuery: '', @@ -60,7 +61,8 @@ export default class Table extends Component void; export interface IFilterFactoryProps { + columns: IColumn[]; filter_query: string; filter_action: TableAction; + filter_case: Case; id: string; map: Map; rawFilterQuery: string; row_deletable: boolean; row_selectable: Selection; setFilter: SetFilter; + setProps: SetProps; style_cell: Style; style_cell_conditional: Cells; style_filter: Style; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 67c225f66..0c054299d 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -47,6 +47,7 @@ export const defaultProps = { css: [], filter_query: '', filter_action: 'none', + filter_case: 'sensitive', sort_as_null: [], sort_action: 'none', sort_mode: 'single', @@ -249,7 +250,6 @@ export const propTypes = { * will select *all* of the merged columns associated with it. * The table-level prop `column_selectable` is used to determine the type of column * selection to use. - * */ selectable: PropTypes.oneOfType([ PropTypes.oneOf(['first', 'last']), @@ -257,6 +257,12 @@ export const propTypes = { PropTypes.arrayOf(PropTypes.bool) ]), + /** + * If not blank, the filter on the column will override the table setting and always be + * case-sensitive, or case-insensitive unless a case-specific operator is used. + */ + filter_case: PropTypes.oneOf(['', 'sensitive', 'insensitive']), + /** * The formatting applied to the column's data. * This prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from @@ -955,13 +961,6 @@ export const propTypes = { */ tooltip_duration: PropTypes.number, - /** - * If `filter_action` is enabled, then the current filtering - * string is represented in this `filter_query` - * property. - */ - filter_query: PropTypes.string, - /** * The `filter_action` property controls the behavior of the `filtering` UI. * If `'none'`, then the filtering UI is not displayed. @@ -975,6 +974,22 @@ export const propTypes = { */ filter_action: PropTypes.oneOf(['custom', 'native', 'none']), + /** + * If `filter_action` is enabled, then the current filtering + * string is represented in this `filter_query` + * property. + */ + filter_query: PropTypes.string, + + /** + * If `filter_action` is enabled, the `filter_case` property controls the case-sensitivity of + * the filters. + * If `'sensitive'`, filtering on all columns will be case-sensitive (default behavior). + * If `'insensitive'`, filtering on all columns will be case-insensitive. + * This setting can be overridden per column. + */ + filter_case: PropTypes.oneOf(['sensitive', 'insensitive']), + /** * The `sort_action` property enables data to be * sorted on a per-column basis. @@ -1142,7 +1157,7 @@ export const propTypes = { * subType (string; optional): * 'open-block': '()', * 'logical-operator': '&&', '||', - * 'relational-operator': '=', '>=', '>', '<=', '<', '!=', 'contains', + * 'relational-operator': '=', '>=', '>', '<=', '<', '!=', 'contains', 'i=', 'i>=', 'i>', 'i<=', 'i<', 'i!=', 'icontains', 's=', 's>=', 's>', 's<=', 's<', 's!=', 'scontains', * 'unary-operator': '!', 'is bool', 'is even', 'is nil', 'is num', 'is object', 'is odd', 'is prime', 'is str', * 'expression': 'value', 'field'; * value (any): diff --git a/src/dash-table/dash/Sanitizer.ts b/src/dash-table/dash/Sanitizer.ts index f6609504a..043a8abae 100644 --- a/src/dash-table/dash/Sanitizer.ts +++ b/src/dash-table/dash/Sanitizer.ts @@ -87,7 +87,7 @@ export default class Sanitizer { headerFormat = ExportHeaders.Ids; } - return R.merge(props, { + return R.mergeRight(props, { columns, export_headers: headerFormat, fixed_columns: getFixedColumns(props.fixed_columns, props.row_deletable, props.row_selectable), diff --git a/src/dash-table/derived/cell/wrapperStyles.ts b/src/dash-table/derived/cell/wrapperStyles.ts index 7ebc7ff22..290d3f452 100644 --- a/src/dash-table/derived/cell/wrapperStyles.ts +++ b/src/dash-table/derived/cell/wrapperStyles.ts @@ -34,7 +34,7 @@ const getter = ( return; } - styles[i][j] = R.merge(styles[i][j], SELECTED_CELL_STYLE); + styles[i][j] = R.mergeRight(styles[i][j], SELECTED_CELL_STYLE); }, selectedCells); return styles; diff --git a/src/dash-table/derived/filter/map.ts b/src/dash-table/derived/filter/map.ts index 0259ebb47..cde842d78 100644 --- a/src/dash-table/derived/filter/map.ts +++ b/src/dash-table/derived/filter/map.ts @@ -2,7 +2,7 @@ import * as R from 'ramda'; import { memoizeOneFactory } from 'core/memoizer'; -import { Columns, IColumn, SetFilter } from 'dash-table/components/Table/props'; +import { Columns, IColumn, SetFilter, Case } from 'dash-table/components/Table/props'; import { SingleColumnSyntaxTree, MultiColumnsSyntaxTree, getMultiColumnQueryString, getSingleColumnMap } from 'dash-table/syntax-tree'; const cloneIf = ( @@ -13,10 +13,11 @@ const cloneIf = ( export default memoizeOneFactory(( map: Map, query: string, - columns: Columns + columns: Columns, + filter_case: Case ): Map => { const multiQuery = new MultiColumnsSyntaxTree(query); - const reversedMap = getSingleColumnMap(multiQuery, columns); + const reversedMap = getSingleColumnMap(multiQuery, columns, filter_case); /* * Couldn't process the query, just use the previous value. @@ -61,12 +62,12 @@ export default memoizeOneFactory(( return newMap; }); -function updateMap(map: Map, column: IColumn, value: any) { +function updateMap(map: Map, column: IColumn, value: any, filter_case: Case) { const id = column.id.toString(); const newMap = new Map(map); if (value && value.length) { - newMap.set(id, new SingleColumnSyntaxTree(value, column)); + newMap.set(id, new SingleColumnSyntaxTree(value, column, filter_case)); } else { newMap.delete(id); } @@ -90,19 +91,21 @@ export const updateColumnFilter = ( map: Map, column: IColumn, value: any, - setFilter: SetFilter + setFilter: SetFilter, + filter_case: Case ) => { - map = updateMap(map, column, value); + map = updateMap(map, column, value, filter_case); updateState(map, setFilter); }; export const clearColumnsFilter = ( map: Map, columns: Columns, - setFilter: SetFilter + setFilter: SetFilter, + filter_case: Case ) => { R.forEach(column => { - map = updateMap(map, column, ''); + map = updateMap(map, column, '', filter_case); }, columns); updateState(map, setFilter); diff --git a/src/dash-table/derived/header/content.tsx b/src/dash-table/derived/header/content.tsx index 4a3a413c6..33f8246ab 100644 --- a/src/dash-table/derived/header/content.tsx +++ b/src/dash-table/derived/header/content.tsx @@ -15,7 +15,8 @@ import { SetFilter, SetProps, SortMode, - TableAction + TableAction, + Case } from 'dash-table/components/Table/props'; import getColumnFlag from 'dash-table/derived/header/columnFlag'; import * as actions from 'dash-table/utils/actions'; @@ -40,7 +41,8 @@ const doAction = ( setFilter: SetFilter, setProps: SetProps, map: Map, - data: Data + data: Data, + filter_case: Case ) => () => { const props = action(column, columns, visibleColumns, columnRowIndex, mergeDuplicateHeaders, data); @@ -64,7 +66,7 @@ const doAction = ( } }, affectedColumIds); - clearColumnsFilter(map, affectedColumns, setFilter); + clearColumnsFilter(map, affectedColumns, setFilter, filter_case); }; function doSort(columnId: ColumnId, sortBy: SortBy, mode: SortMode, setProps: SetProps) { @@ -165,7 +167,8 @@ function getter( paginationMode: TableAction, setFilter: SetFilter, setProps: SetProps, - mergeDuplicateHeaders: boolean + mergeDuplicateHeaders: boolean, + filter_case: Case ): JSX.Element[][] { return R.addIndex, JSX.Element[]>(R.map)( ([labels, indices], headerRowIndex) => { @@ -263,7 +266,7 @@ function getter( null : ( ) @@ -275,7 +278,7 @@ function getter( className={'column-header--delete' + (spansAllColumns ? ' disabled' : '')} onClick={spansAllColumns ? undefined : - doAction(actions.deleteColumn, selected_columns, column, columns, visibleColumns, headerRowIndex, mergeDuplicateHeaders, setFilter, setProps, map, data) + doAction(actions.deleteColumn, selected_columns, column, columns, visibleColumns, headerRowIndex, mergeDuplicateHeaders, setFilter, setProps, map, data, filter_case) } > diff --git a/src/dash-table/derived/table/index.tsx b/src/dash-table/derived/table/index.tsx index 7d3ed773e..6e4afcb87 100644 --- a/src/dash-table/derived/table/index.tsx +++ b/src/dash-table/derived/table/index.tsx @@ -25,7 +25,7 @@ const handleSetFilter = ( function propsAndMapFn(propsFn: () => ControlledTableProps, setFilter: any) { const props = propsFn(); - return R.merge(props, { map: props.workFilter.map, setFilter }); + return R.mergeRight(props, { map: props.workFilter.map, setFilter }); } export default (propsFn: () => ControlledTableProps) => { diff --git a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts index 9c024ec5d..71667b239 100644 --- a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts +++ b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts @@ -1,12 +1,14 @@ +import * as R from 'ramda'; + import { RequiredPluck, OptionalPluck } from 'core/type'; import SyntaxTree from 'core/syntax-tree'; import { ILexemeResult, ILexerResult } from 'core/syntax-tree/lexer'; import { LexemeType, boundLexeme } from 'core/syntax-tree/lexicon'; -import { ColumnType, IColumn } from 'dash-table/components/Table/props'; +import { ColumnType, IColumn, Case } from 'dash-table/components/Table/props'; import { fieldExpression } from './lexeme/expression'; -import { equal, RelationalOperator, contains, dateStartsWith } from './lexeme/relational'; +import { equal, RelationalOperator, contains, dateStartsWith, CaseMapping } from './lexeme/relational'; import columnLexicon from './lexicon/column'; @@ -45,7 +47,16 @@ function isUnary(lexemes: ILexemeResult[]) { lexemes[0].lexeme.type === LexemeType.UnaryOperator; } -function modifyLex(config: SingleColumnConfig, res: ILexerResult) { +function isRelational(lexemes: ILexemeResult[]) { + return lexemes.length === 3 && + lexemes[1].lexeme.type === LexemeType.RelationalOperator; +} + +function isDefaultCase(lexemes: ILexemeResult[]) { + return lexemes[1].lexeme.case === 'default'; +} + +function modifyLex(config: SingleColumnConfig, computed_filter_case: Case, res: ILexerResult) { if (!res.valid) { return res; } @@ -63,17 +74,28 @@ function modifyLex(config: SingleColumnConfig, res: ILexerResult) { ]; } + if ((computed_filter_case === 'insensitive') && isRelational(res.lexemes) && isDefaultCase(res.lexemes)) { + const replacement: ILexemeResult = + CaseMapping.get(res.lexemes[1].lexeme.subType as RelationalOperator) as ILexemeResult; + res.lexemes[1] = { + lexeme: R.mergeRight(res.lexemes[1].lexeme, replacement.lexeme), + value: replacement.value + }; + } + return res; } -export type SingleColumnConfig = RequiredPluck & OptionalPluck; +export type SingleColumnConfig = RequiredPluck & + OptionalPluck & + OptionalPluck; export default class SingleColumnSyntaxTree extends SyntaxTree { - constructor(query: string, config: SingleColumnConfig) { + constructor(query: string, config: SingleColumnConfig, computed_filter_case: Case) { super( columnLexicon, query, - modifyLex.bind(undefined, config) + modifyLex.bind(undefined, config, computed_filter_case) ); } } \ No newline at end of file diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index e4d03607c..a451608c6 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -6,7 +6,7 @@ import MultiColumnsSyntaxTree from './MultiColumnsSyntaxTree'; import QuerySyntaxTree from './QuerySyntaxTree'; import SingleColumnSyntaxTree from './SingleColumnSyntaxTree'; import { RelationalOperator } from './lexeme/relational'; -import { IColumn } from 'dash-table/components/Table/props'; +import { IColumn, Case } from 'dash-table/components/Table/props'; export const getMultiColumnQueryString = ( asts: SingleColumnSyntaxTree[] @@ -17,7 +17,8 @@ export const getMultiColumnQueryString = ( export const getSingleColumnMap = ( ast: MultiColumnsSyntaxTree, - columns: IColumn[] + columns: IColumn[], + filter_case: Case ) => { if (!ast.isValid) { return; @@ -39,7 +40,7 @@ export const getSingleColumnMap = ( throw new Error(`column ${sanitizedColumnId} not found`); } - map.set(sanitizedColumnId, new SingleColumnSyntaxTree(s.value, column)); + map.set(sanitizedColumnId, new SingleColumnSyntaxTree(s.value, column, filter_case)); } else if (s.lexeme.type === LexemeType.RelationalOperator && s.left && s.right) { const sanitizedColumnId = s.left.lexeme.present ? s.left.lexeme.present(s.left) : s.left.value; @@ -49,9 +50,9 @@ export const getSingleColumnMap = ( } if (s.lexeme.present && s.lexeme.present(s) === RelationalOperator.Equal) { - map.set(sanitizedColumnId, new SingleColumnSyntaxTree(`${s.right.value}`, column)); + map.set(sanitizedColumnId, new SingleColumnSyntaxTree(`${s.right.value}`, column, filter_case)); } else { - map.set(sanitizedColumnId, new SingleColumnSyntaxTree(`${s.value} ${s.right.value}`, column)); + map.set(sanitizedColumnId, new SingleColumnSyntaxTree(`${s.value} ${s.right.value}`, column, filter_case)); } } }, statements); diff --git a/src/dash-table/syntax-tree/lexeme/relational.ts b/src/dash-table/syntax-tree/lexeme/relational.ts index bbf354b0a..8e0298d3e 100644 --- a/src/dash-table/syntax-tree/lexeme/relational.ts +++ b/src/dash-table/syntax-tree/lexeme/relational.ts @@ -40,7 +40,21 @@ export enum RelationalOperator { GreaterThan = '>', LessOrEqual = '<=', LessThan = '<', - NotEqual = '!=' + NotEqual = '!=', + IContains = 'icontains', + IEqual = 'i=', + IGreaterOrEqual = 'i>=', + IGreaterThan = 'i>', + ILessOrEqual = 'i<=', + ILessThan = 'i<', + INotEqual = 'i!=', + SContains = 'scontains', + SEqual = 's=', + SGreaterOrEqual = 's>=', + SGreaterThan = 's>', + SLessOrEqual = 's<=', + SLessThan = 's<', + SNotEqual = 's!=' } const LEXEME_BASE = { @@ -49,7 +63,7 @@ const LEXEME_BASE = { type: LexemeType.RelationalOperator }; -export const contains: IUnboundedLexeme = R.merge({ +export const contains: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(([op, exp]) => !R.isNil(exp) && !R.isNil(op) && @@ -57,31 +71,35 @@ export const contains: IUnboundedLexeme = R.merge({ op.toString().indexOf(exp.toString()) !== -1 ), subType: RelationalOperator.Contains, + case: 'default', regexp: /^((contains)(?=\s|$))/i, regexpMatch: 1 }, LEXEME_BASE); -export const equal: IUnboundedLexeme = R.merge({ +export const equal: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(([op, exp]) => (isNumeric(op) && isNumeric(exp)) ? +op === +exp : op === exp ), subType: RelationalOperator.Equal, + case: 'default', regexp: /^(=|(eq)(?=\s|$))/i, regexpMatch: 1 }, LEXEME_BASE); -export const greaterOrEqual: IUnboundedLexeme = R.merge({ +export const greaterOrEqual: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(([op, exp]) => op >= exp), subType: RelationalOperator.GreaterOrEqual, + case: 'default', regexp: /^(>=|(ge)(?=\s|$))/i, regexpMatch: 1 }, LEXEME_BASE); -export const greaterThan: IUnboundedLexeme = R.merge({ +export const greaterThan: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(([op, exp]) => op > exp), subType: RelationalOperator.GreaterThan, + case: 'default', regexp: /^(>|(gt)(?=\s|$))/i, regexpMatch: 1 }, LEXEME_BASE); @@ -90,7 +108,7 @@ const DATE_OPTIONS: IDateValidation = { allow_YY: true }; -export const dateStartsWith: IUnboundedLexeme = R.merge({ +export const dateStartsWith: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(([op, exp]) => { op = typeof op === 'number' ? op.toString() : op; exp = typeof exp === 'number' ? exp.toString() : exp; @@ -108,23 +126,199 @@ export const dateStartsWith: IUnboundedLexeme = R.merge({ regexpMatch: 1 }, LEXEME_BASE); -export const lessOrEqual: IUnboundedLexeme = R.merge({ +export const lessOrEqual: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(([op, exp]) => op <= exp), subType: RelationalOperator.LessOrEqual, + case: 'default', regexp: /^(<=|(le)(?=\s|$))/i, regexpMatch: 1 }, LEXEME_BASE); -export const lessThan: IUnboundedLexeme = R.merge({ +export const lessThan: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(([op, exp]) => op < exp), subType: RelationalOperator.LessThan, + case: 'default', regexp: /^(<|(lt)(?=\s|$))/i, regexpMatch: 1 }, LEXEME_BASE); -export const notEqual: IUnboundedLexeme = R.merge({ +export const notEqual: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(([op, exp]) => op !== exp), subType: RelationalOperator.NotEqual, + case: 'default', regexp: /^(!=|(ne)(?=\s|$))/i, regexpMatch: 1 }, LEXEME_BASE); + +export const icontains: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + !R.isNil(exp) && + !R.isNil(op) && + (R.type(exp) === 'String' || R.type(op) === 'String') && + op.toString().toLowerCase().indexOf(exp.toString().toLowerCase()) !== -1 + ), + subType: RelationalOperator.IContains, + case: 'insensitive', + regexp: /^((icontains)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const iequal: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op === +exp : + op.toString().toLowerCase() === exp.toString().toLowerCase() + ), + subType: RelationalOperator.IEqual, + case: 'insensitive', + regexp: /^(i=|(ieq)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const igreaterOrEqual: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op >= +exp : + op.toString().toLowerCase() >= exp.toString().toLowerCase()), + subType: RelationalOperator.IGreaterOrEqual, + case: 'insensitive', + regexp: /^(i>=|(ige)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const igreaterThan: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op > +exp : + op.toString().toLowerCase() > exp.toString().toLowerCase()), + subType: RelationalOperator.IGreaterThan, + case: 'insensitive', + regexp: /^(i>|(igt)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const ilessOrEqual: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op <= +exp : + op.toString().toLowerCase() <= exp.toString().toLowerCase()), + subType: RelationalOperator.ILessOrEqual, + case: 'insensitive', + regexp: /^(i<=|(ile)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const ilessThan: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op < +exp : + op.toString().toLowerCase() < exp.toString().toLowerCase()), + subType: RelationalOperator.ILessThan, + case: 'insensitive', + regexp: /^(i<|(ilt)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const inotEqual: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op !== +exp : + op.toString().toLowerCase() !== exp.toString().toLowerCase()), + subType: RelationalOperator.INotEqual, + case: 'insensitive', + regexp: /^(i!=|(ine)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const scontains: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + !R.isNil(exp) && + !R.isNil(op) && + (R.type(exp) === 'String' || R.type(op) === 'String') && + op.toString().indexOf(exp.toString()) !== -1 + ), + subType: RelationalOperator.SContains, + case: 'insensitive', + regexp: /^((scontains)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const sequal: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op === +exp : + op === exp + ), + sType: RelationalOperator.SEqual, + case: 'insensitive', + regexp: /^(s=|(seq)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const sgreaterOrEqual: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op >= +exp : + op >= exp), + subType: RelationalOperator.SGreaterOrEqual, + case: 'sensitive', + regexp: /^(s>=|(sge)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const sgreaterThan: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op > +exp : + op > exp), + subType: RelationalOperator.SGreaterThan, + case: 'sensitive', + regexp: /^(s>|(sgt)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const slessOrEqual: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op <= +exp : + op <= exp), + subType: RelationalOperator.SLessOrEqual, + case: 'sensitive', + regexp: /^(s<=|(sle)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const slessThan: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op < +exp : + op < exp), + subType: RelationalOperator.SLessThan, + case: 'sensitive', + regexp: /^(s<|(slt)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const snotEqual: IUnboundedLexeme = R.mergeRight({ + evaluate: relationalEvaluator(([op, exp]) => + (isNumeric(op) && isNumeric(exp)) ? + +op !== +exp : + op !== exp), + subType: RelationalOperator.SNotEqual, + case: 'sensitive', + regexp: /^(s!=|(sne)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); + +export const CaseMapping: Map = new Map([ + [RelationalOperator.Contains, { lexeme: icontains, value: 'icontains' }], + [RelationalOperator.Equal, { lexeme: iequal, value: 'i=' }], + [RelationalOperator.GreaterOrEqual, { lexeme: igreaterOrEqual, value: 'i>=' }], + [RelationalOperator.GreaterThan, { lexeme: igreaterThan, value: 'i>' }], + [RelationalOperator.LessOrEqual, { lexeme: ilessOrEqual, value: 'i<=' }], + [RelationalOperator.LessThan, { lexeme: ilessThan, value: 'i<' }], + [RelationalOperator.NotEqual, { lexeme: inotEqual, value: 'i!=' }] +]); diff --git a/src/dash-table/syntax-tree/lexeme/unary.ts b/src/dash-table/syntax-tree/lexeme/unary.ts index 67a969ecf..a201dd07f 100644 --- a/src/dash-table/syntax-tree/lexeme/unary.ts +++ b/src/dash-table/syntax-tree/lexeme/unary.ts @@ -65,47 +65,47 @@ export const not: IUnboundedLexeme = { } }; -export const isBool: IUnboundedLexeme = R.merge({ +export const isBool: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(opValue => typeof opValue === 'boolean'), regexp: /^(is bool)/i }, LEXEME_BASE); -export const isEven: IUnboundedLexeme = R.merge({ +export const isEven: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(opValue => typeof opValue === 'number' && opValue % 2 === 0), regexp: /^(is even)/i }, LEXEME_BASE); -export const isBlank: IUnboundedLexeme = R.merge({ +export const isBlank: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(opValue => opValue === undefined || opValue === null || opValue === ''), regexp: /^(is blank)/i }, LEXEME_BASE); -export const isNil: IUnboundedLexeme = R.merge({ +export const isNil: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(opValue => opValue === undefined || opValue === null), regexp: /^(is nil)/i }, LEXEME_BASE); -export const isNum: IUnboundedLexeme = R.merge({ +export const isNum: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(opValue => typeof opValue === 'number'), regexp: /^(is num)/i }, LEXEME_BASE); -export const isObject: IUnboundedLexeme = R.merge({ +export const isObject: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(opValue => opValue !== null && typeof opValue === 'object'), regexp: /^(is object)/i }, LEXEME_BASE); -export const isOdd: IUnboundedLexeme = R.merge({ +export const isOdd: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(opValue => typeof opValue === 'number' && opValue % 2 === 1), regexp: /^(is odd)/i }, LEXEME_BASE); -export const isPrime: IUnboundedLexeme = R.merge({ +export const isPrime: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(opValue => typeof opValue === 'number' && checkPrimality(opValue)), regexp: /^(is prime)/i }, LEXEME_BASE); -export const isStr: IUnboundedLexeme = R.merge({ +export const isStr: IUnboundedLexeme = R.mergeRight({ evaluate: relationalEvaluator(opValue => typeof opValue === 'string'), regexp: /^(is str)/i }, LEXEME_BASE); \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index e8bfaf08a..b75ccfffb 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -11,7 +11,21 @@ import { greaterThan, lessOrEqual, lessThan, - notEqual + notEqual, + icontains, + iequal, + igreaterOrEqual, + igreaterThan, + ilessOrEqual, + ilessThan, + inotEqual, + scontains, + sequal, + sgreaterOrEqual, + sgreaterThan, + slessOrEqual, + slessThan, + snotEqual } from '../lexeme/relational'; import { isBlank, @@ -40,7 +54,21 @@ const lexicon: ILexeme[] = [ greaterThan, lessOrEqual, lessThan, - notEqual + notEqual, + icontains, + iequal, + igreaterOrEqual, + igreaterThan, + ilessOrEqual, + ilessThan, + inotEqual, + scontains, + sequal, + sgreaterOrEqual, + sgreaterThan, + slessOrEqual, + slessThan, + snotEqual ].map(op => ({ ...op, if: ifLeading, diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index f3e138fee..4a5703bf8 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -16,7 +16,21 @@ import { greaterThan, lessOrEqual, lessThan, - notEqual + notEqual, + icontains, + iequal, + igreaterOrEqual, + igreaterThan, + ilessOrEqual, + ilessThan, + inotEqual, + scontains, + sequal, + sgreaterOrEqual, + sgreaterThan, + slessOrEqual, + slessThan, + snotEqual } from '../lexeme/relational'; import { isBlank, @@ -51,7 +65,21 @@ const lexicon: ILexeme[] = [ greaterThan, lessOrEqual, lessThan, - notEqual + notEqual, + icontains, + iequal, + igreaterOrEqual, + igreaterThan, + ilessOrEqual, + ilessThan, + inotEqual, + scontains, + sequal, + sgreaterOrEqual, + sgreaterThan, + slessOrEqual, + slessThan, + snotEqual ].map(op => ({ ...op, if: ifRelationalOperator, diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index a53698a65..467c67027 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -24,7 +24,21 @@ import { greaterThan, lessOrEqual, lessThan, - notEqual + notEqual, + icontains, + iequal, + igreaterOrEqual, + igreaterThan, + ilessOrEqual, + ilessThan, + inotEqual, + scontains, + sequal, + sgreaterOrEqual, + sgreaterThan, + slessOrEqual, + slessThan, + snotEqual } from '../lexeme/relational'; import { isBlank, @@ -84,7 +98,21 @@ const lexicon: ILexeme[] = [ greaterThan, lessOrEqual, lessThan, - notEqual + notEqual, + icontains, + iequal, + igreaterOrEqual, + igreaterThan, + ilessOrEqual, + ilessThan, + inotEqual, + scontains, + sequal, + sgreaterOrEqual, + sgreaterThan, + slessOrEqual, + slessThan, + snotEqual ].map(op => ({ ...op, if: ifRelationalOperator, diff --git a/tests/cypress/src/DashTable.ts b/tests/cypress/src/DashTable.ts index 2cde2eec2..9f9b80f57 100644 --- a/tests/cypress/src/DashTable.ts +++ b/tests/cypress/src/DashTable.ts @@ -40,6 +40,10 @@ export class DashTableHelper { return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-filter[data-dash-column="${column}"]`); } + public getFilterCaseById(column: string, editable: State = State.Ready) { + return this.getFilterById(column, editable).within(() => cy.get('input[type=button]')); + } + public getHeader(row: number, column: number, editable: State = State.Ready) { return cy.get(`#${this.id} ${getSelector(editable)} tbody tr th.dash-header.column-${column}`).eq(row); } diff --git a/tests/cypress/tests/server/select_props_test.ts b/tests/cypress/tests/server/select_props_test.ts index f018dd180..5f20e2d2e 100644 --- a/tests/cypress/tests/server/select_props_test.ts +++ b/tests/cypress/tests/server/select_props_test.ts @@ -137,11 +137,11 @@ describe('select row', () => { }); it('selection props are correct, with filter', () => { - DashTable.getSelect(0).within(() => cy.get('input').click()); - DashTable.getSelect(1).within(() => cy.get('input').click()); - DashTable.getSelect(2).within(() => cy.get('input').click()); + DashTable.getSelect(0).within(() => cy.get('input[type=text]').click()); + DashTable.getSelect(1).within(() => cy.get('input[type=text]').click()); + DashTable.getSelect(2).within(() => cy.get('input[type=text]').click()); - cy.get('tr th.column-0.dash-filter input').type(`is even${Key.Enter}`); + cy.get('tr th.column-0.dash-filter input[type=text]').type(`is even${Key.Enter}`); // filtered-out data is still selected expectArray('#selected_rows_container', [0, 1, 2]); @@ -157,13 +157,13 @@ describe('select row', () => { }); it('selection props are correct, with filter & sort', () => { - DashTable.getSelect(0).within(() => cy.get('input').click()); - DashTable.getSelect(1).within(() => cy.get('input').click()); + DashTable.getSelect(0).within(() => cy.get('input[type=text]').click()); + DashTable.getSelect(1).within(() => cy.get('input[type=text]').click()); DashTable.getCell(3, 1).click(); expectCellSelection([3], [3003], [1], [1]); - cy.get('tr th.column-0.dash-filter input').type(`is even${Key.Enter}`); + cy.get('tr th.column-0.dash-filter input[type=text]').type(`is even${Key.Enter}`); expectCellSelection([]); diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts index 23e41fe1c..3d37501ad 100644 --- a/tests/cypress/tests/standalone/filtering_test.ts +++ b/tests/cypress/tests/standalone/filtering_test.ts @@ -189,4 +189,20 @@ describe('filter', () => { DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', '')); DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', '')); }); + + it.only('sets column filter to case insensitive', () => { + let cell_0; + + DashTable.getCellById(0, 'bbb') + .within(() => cy.get('.Select-value-label') + .then($el => cell_0 = $el[0].innerHTML)); + + DashTable.getFilterCaseById('bbb').click(); + DashTable.getFilterById('bbb').click(); + DOM.focused.type(`wet`); + DashTable.getFilterById('ggg').click(); + DashTable.getCellById(0, 'bbb').within(() => cy.get('.Select-value-label').should('have.html', cell_0)); + DashTable.getFilterCaseById('bbb').click(); + cy.get('.row-1').within(() => cy.get('tr').should('not.exist')); + }); }); \ No newline at end of file diff --git a/tests/cypress/tests/unit/query_syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts index 6cddb8572..f6b2ea2c4 100644 --- a/tests/cypress/tests/unit/query_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -599,7 +599,96 @@ describe('Query Syntax Tree', () => { const tree = new QuerySyntaxTree('{a}<=5'); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 4 })).to.equal(true); + }); + + it('can do case insensitive equality (i=) test', () => { + const tree = new QuerySyntaxTree('{a} i= "abc v"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(true); + expect(tree.evaluate({ a: 'AbC V' })).to.equal(true); + expect(tree.evaluate({ a: 'abc w' })).to.equal(false); + }); + + it('can do case insensitive equality (ieq) test', () => { + const tree = new QuerySyntaxTree('{a} ieq "abc v"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(true); + expect(tree.evaluate({ a: 'AbC V' })).to.equal(true); + expect(tree.evaluate({ a: 'abc w' })).to.equal(false); + }); + it('can do case insensitive difference (ine) test', () => { + const tree = new QuerySyntaxTree('{a} ine "abc v"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(false); + expect(tree.evaluate({ a: 'AbC V' })).to.equal(false); + expect(tree.evaluate({ a: 'abc w' })).to.equal(true); + }); + + it('can do case insensitive difference (i!=) test', () => { + const tree = new QuerySyntaxTree('{a} i!= "abc v"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(false); + expect(tree.evaluate({ a: 'AbC V' })).to.equal(false); + expect(tree.evaluate({ a: 'abc w' })).to.equal(true); + }); + + it('can do case insensitive icontains (icontains) test', () => { + const tree = new QuerySyntaxTree('{a} icontains v'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(true); + expect(tree.evaluate({ a: 'abc V' })).to.equal(true); + expect(tree.evaluate({ a: 'abc w' })).to.equal(false); + }); + + it('can do case sensitive equality (s=) test', () => { + const tree = new QuerySyntaxTree('{a} s= "abc v"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(true); + expect(tree.evaluate({ a: 'AbC V' })).to.equal(false); + expect(tree.evaluate({ a: 'abc w' })).to.equal(false); + }); + + it('can do case sensitive equality (seq) test', () => { + const tree = new QuerySyntaxTree('{a} seq "abc v"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(true); + expect(tree.evaluate({ a: 'AbC V' })).to.equal(false); + expect(tree.evaluate({ a: 'abc w' })).to.equal(false); + }); + + it('can do case sensitive difference (sne) test', () => { + const tree = new QuerySyntaxTree('{a} sne "abc v"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(false); + expect(tree.evaluate({ a: 'AbC V' })).to.equal(true); + expect(tree.evaluate({ a: 'abc w' })).to.equal(true); + }); + + it('can do case sensitive difference (s!=) test', () => { + const tree = new QuerySyntaxTree('{a} s!= "abc v"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(false); + expect(tree.evaluate({ a: 'AbC V' })).to.equal(true); + expect(tree.evaluate({ a: 'abc w' })).to.equal(true); + }); + + it('can do case sensitive scontains (scontains) test', () => { + const tree = new QuerySyntaxTree('{a} scontains v'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(true); + expect(tree.evaluate({ a: 'abc V' })).to.equal(false); + expect(tree.evaluate({ a: 'abc w' })).to.equal(false); }); }); }); diff --git a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts index 5218ced39..7517212b7 100644 --- a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts @@ -1,5 +1,5 @@ import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; -import { ColumnType } from 'dash-table/components/Table/props'; +import { ColumnType, Case } from 'dash-table/components/Table/props'; import { SingleColumnConfig } from 'dash-table/syntax-tree/SingleColumnSyntaxTree'; import { RelationalOperator } from 'dash-table/syntax-tree/lexeme/relational'; import { LexemeType } from 'core/syntax-tree/lexicon'; @@ -29,34 +29,46 @@ const COLUMN_UNDEFINED: SingleColumnConfig = { type: undefined }; +const COLUMN_CASE_INSENSITIVE: SingleColumnConfig = { + id: 'a', + type: ColumnType.Text, + filter_case: Case.Insensitive +}; + +const COLUMN_CASE_SENSITIVE: SingleColumnConfig = { + id: 'a', + type: ColumnType.Text, + filter_case: Case.Sensitive +}; + describe('Single Column Syntax Tree', () => { it('cannot have operand', () => { - const tree = new SingleColumnSyntaxTree('{a} <= 1', COLUMN_UNDEFINED); + const tree = new SingleColumnSyntaxTree('{a} <= 1', COLUMN_UNDEFINED, Case.Sensitive); expect(tree.isValid).to.equal(false); }); it('cannot have binary dangle', () => { - const tree = new SingleColumnSyntaxTree('<=', COLUMN_UNDEFINED); + const tree = new SingleColumnSyntaxTree('<=', COLUMN_UNDEFINED, Case.Sensitive); expect(tree.isValid).to.equal(false); }); it('cannot be unary + expression', () => { - const tree = new SingleColumnSyntaxTree('is prime "a"', COLUMN_UNDEFINED); + const tree = new SingleColumnSyntaxTree('is prime "a"', COLUMN_UNDEFINED, Case.Sensitive); expect(tree.isValid).to.equal(false); }); it('can be empty', () => { - const tree = new SingleColumnSyntaxTree('', COLUMN_UNDEFINED); + const tree = new SingleColumnSyntaxTree('', COLUMN_UNDEFINED, Case.Sensitive); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 0 })).to.equal(true); }); it('can be binary + expression', () => { - const tree = new SingleColumnSyntaxTree('<= 1', COLUMN_UNDEFINED); + const tree = new SingleColumnSyntaxTree('<= 1', COLUMN_UNDEFINED, Case.Sensitive); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 0 })).to.equal(true); @@ -66,7 +78,7 @@ describe('Single Column Syntax Tree', () => { }); it('can be unary', () => { - const tree = new SingleColumnSyntaxTree('is prime', COLUMN_UNDEFINED); + const tree = new SingleColumnSyntaxTree('is prime', COLUMN_UNDEFINED, Case.Sensitive); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 5 })).to.equal(true); @@ -76,7 +88,7 @@ describe('Single Column Syntax Tree', () => { }); it('can be expression with undefined column type', () => { - const tree = new SingleColumnSyntaxTree('1', COLUMN_UNDEFINED); + const tree = new SingleColumnSyntaxTree('1', COLUMN_UNDEFINED, Case.Sensitive); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: '1' })).to.equal(true); @@ -88,7 +100,7 @@ describe('Single Column Syntax Tree', () => { }); it('can be expression with numeric column type', () => { - const tree = new SingleColumnSyntaxTree('1', COLUMN_NUMERIC); + const tree = new SingleColumnSyntaxTree('1', COLUMN_NUMERIC, Case.Sensitive); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 1 })).to.equal(true); @@ -98,7 +110,7 @@ describe('Single Column Syntax Tree', () => { }); it.only('can be permissive value expression', () => { - const tree = new SingleColumnSyntaxTree('Hello world', COLUMN_TEXT); + const tree = new SingleColumnSyntaxTree('Hello world', COLUMN_TEXT, Case.Sensitive); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 'Hello world' })).to.equal(true); @@ -107,25 +119,25 @@ describe('Single Column Syntax Tree', () => { }); it('`undefined` column type can use `contains`', () => { - const tree = new SingleColumnSyntaxTree('contains 1', COLUMN_UNDEFINED); + const tree = new SingleColumnSyntaxTree('contains 1', COLUMN_UNDEFINED, Case.Sensitive); expect(tree.isValid).to.equal(true); }); it('`any` column type can use `contains`', () => { - const tree = new SingleColumnSyntaxTree('contains 1', COLUMN_ANY); + const tree = new SingleColumnSyntaxTree('contains 1', COLUMN_ANY, Case.Sensitive); expect(tree.isValid).to.equal(true); }); it('`numeric` column type can use `contains`', () => { - const tree = new SingleColumnSyntaxTree('contains 1', COLUMN_NUMERIC); + const tree = new SingleColumnSyntaxTree('contains 1', COLUMN_NUMERIC, Case.Sensitive); expect(tree.isValid).to.equal(true); }); it('can be expression with text column type', () => { - const tree = new SingleColumnSyntaxTree('"1"', COLUMN_TEXT); + const tree = new SingleColumnSyntaxTree('"1"', COLUMN_TEXT, Case.Sensitive); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 1 })).to.equal(true); @@ -138,7 +150,7 @@ describe('Single Column Syntax Tree', () => { ['1975', '"1975"'].forEach(value => { it(`can be expression '${value}' with datetime column type`, () => { - const tree = new SingleColumnSyntaxTree(value, COLUMN_DATE); + const tree = new SingleColumnSyntaxTree(value, COLUMN_DATE, Case.Sensitive); expect(tree.evaluate({ a: 1975 })).to.equal(true); expect(tree.evaluate({ a: '1975' })).to.equal(true); @@ -160,7 +172,7 @@ describe('Single Column Syntax Tree', () => { { type: COLUMN_TEXT, name: 'text' } ].forEach(({ type, name }) => { it(`returns the correct relational operator lexeme for '${name}' column type`, () => { - const tree = new SingleColumnSyntaxTree('1', type); + const tree = new SingleColumnSyntaxTree('1', type, Case.Sensitive); const structure = tree.toStructure(); expect(tree.toQueryString()).to.equal('{a} contains 1'); @@ -189,7 +201,7 @@ describe('Single Column Syntax Tree', () => { }); it(`returns the correct relational operator lexeme for 'date' column type`, () => { - const tree = new SingleColumnSyntaxTree('1975', COLUMN_DATE); + const tree = new SingleColumnSyntaxTree('1975', COLUMN_DATE, Case.Sensitive); const structure = tree.toStructure(); expect(tree.toQueryString()).to.equal('{a} datestartswith 1975'); @@ -217,7 +229,7 @@ describe('Single Column Syntax Tree', () => { }); it(`returns the correct relational operator lexeme for 'numeric' column type`, () => { - const tree = new SingleColumnSyntaxTree('1', COLUMN_NUMERIC); + const tree = new SingleColumnSyntaxTree('1', COLUMN_NUMERIC, Case.Default); const structure = tree.toStructure(); expect(tree.toQueryString()).to.equal('{a} = 1'); @@ -243,4 +255,40 @@ describe('Single Column Syntax Tree', () => { } } }); + + it('can have case-insensitive column', () => { + const tree = new SingleColumnSyntaxTree('= Hello world', COLUMN_CASE_INSENSITIVE, Case.Default); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'Hello world' })).to.equal(true); + expect(tree.evaluate({ a: 'Helloworld' })).to.equal(false); + expect(tree.toQueryString()).to.equal('{a} i= "Hello world"'); + }); + + it('can have forced case-sensitive column in case-insensitive table', () => { + const tree = new SingleColumnSyntaxTree('= Hello world', COLUMN_CASE_SENSITIVE, Case.Insensitive); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'Hello world' })).to.equal(true); + expect(tree.evaluate({ a: 'Helloworld' })).to.equal(false); + expect(tree.toQueryString()).to.equal('{a} = "Hello world"'); + }); + + it('can have forced case-sensitive operator in case-insensitive column', () => { + const tree = new SingleColumnSyntaxTree('c= Hello world', COLUMN_CASE_INSENSITIVE, Case.Default); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'Hello world' })).to.equal(true); + expect(tree.evaluate({ a: 'Helloworld' })).to.equal(false); + expect(tree.toQueryString()).to.equal('{a} = "Hello world"'); + }); + + it('can have forced case-sensitive operator in case-insensitive table', () => { + const tree = new SingleColumnSyntaxTree('c= Hello world', COLUMN_CASE_SENSITIVE, Case.Insensitive); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'Hello world' })).to.equal(true); + expect(tree.evaluate({ a: 'Helloworld' })).to.equal(false); + expect(tree.toQueryString()).to.equal('{a} = "Hello world"'); + }); }); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index 0c76a64fb..3ce83eefa 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -357,7 +357,7 @@ storiesOf('DashTable/Style type condition', module) .add('paging', () => ( R.merge(col, { + columns={ mock.columns.map((col: any) => R.mergeRight(col, { name: col.name, deletable: true }))} diff --git a/tests/visual/percy-storybook/Width.empty.percy.tsx b/tests/visual/percy-storybook/Width.empty.percy.tsx index a1617933e..e492a836e 100644 --- a/tests/visual/percy-storybook/Width.empty.percy.tsx +++ b/tests/visual/percy-storybook/Width.empty.percy.tsx @@ -39,22 +39,22 @@ storiesOf('DashTable/Empty', module) {...props} />)) .add('with column filters -- invalid query', () => ()) .add('with column filters -- single query', () => ()) .add('with column filters -- multi query', () => ()) .add('with column filters -- multi query, no data', () => ()); \ No newline at end of file