diff --git a/CHANGELOG.md b/CHANGELOG.md index 35467a070..82ad6d64f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Include input box for user to navigate directly to a page ### Fixed + [#460](https://github.com/plotly/dash-table/issues/460) - The `datestartswith` relational operator now supports number comparison - Fixed a bug where the implicit operator for columns was `equal` instead of the expected default for the column type @@ -34,6 +35,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). [#546](https://github.com/plotly/dash-table/issues/546) - Visible columns are used correctly for both header and data rows +[#563](https://github.com/plotly/dash-table/issues/563) +- Fixed a bug where any string beginning with a relational operator was being interpreted as that operator being applied to the rest of the string (e.g., "lens" was interpreted as "<=ns") + [#591](https://github.com/plotly/dash-table/issues/591) - Fixed row and column selection when multiple tables are present diff --git a/src/dash-table/syntax-tree/lexeme/relational.ts b/src/dash-table/syntax-tree/lexeme/relational.ts index 06eec9129..bbf354b0a 100644 --- a/src/dash-table/syntax-tree/lexeme/relational.ts +++ b/src/dash-table/syntax-tree/lexeme/relational.ts @@ -57,7 +57,8 @@ export const contains: IUnboundedLexeme = R.merge({ op.toString().indexOf(exp.toString()) !== -1 ), subType: RelationalOperator.Contains, - regexp: /^(contains)/i + regexp: /^((contains)(?=\s|$))/i, + regexpMatch: 1 }, LEXEME_BASE); export const equal: IUnboundedLexeme = R.merge({ @@ -67,19 +68,22 @@ export const equal: IUnboundedLexeme = R.merge({ op === exp ), subType: RelationalOperator.Equal, - regexp: /^(=|eq)/i + regexp: /^(=|(eq)(?=\s|$))/i, + regexpMatch: 1 }, LEXEME_BASE); export const greaterOrEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op >= exp), subType: RelationalOperator.GreaterOrEqual, - regexp: /^(>=|ge)/i + regexp: /^(>=|(ge)(?=\s|$))/i, + regexpMatch: 1 }, LEXEME_BASE); export const greaterThan: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op > exp), subType: RelationalOperator.GreaterThan, - regexp: /^(>|gt)/i + regexp: /^(>|(gt)(?=\s|$))/i, + regexpMatch: 1 }, LEXEME_BASE); const DATE_OPTIONS: IDateValidation = { @@ -100,23 +104,27 @@ export const dateStartsWith: IUnboundedLexeme = R.merge({ normalizedOp.indexOf(normalizedExp) === 0; }), subType: RelationalOperator.DateStartsWith, - regexp: /^(datestartswith)/i + regexp: /^((datestartswith)(?=\s|$))/i, + regexpMatch: 1 }, LEXEME_BASE); export const lessOrEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op <= exp), subType: RelationalOperator.LessOrEqual, - regexp: /^(<=|le)/i + regexp: /^(<=|(le)(?=\s|$))/i, + regexpMatch: 1 }, LEXEME_BASE); export const lessThan: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op < exp), subType: RelationalOperator.LessThan, - regexp: /^(<|lt)/i + regexp: /^(<|(lt)(?=\s|$))/i, + regexpMatch: 1 }, LEXEME_BASE); export const notEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op !== exp), subType: RelationalOperator.NotEqual, - regexp: /^(!=|ne)/i -}, LEXEME_BASE); \ No newline at end of file + regexp: /^(!=|(ne)(?=\s|$))/i, + regexpMatch: 1 +}, LEXEME_BASE); diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts index 6a23a6e62..23e41fe1c 100644 --- a/tests/cypress/tests/standalone/filtering_test.ts +++ b/tests/cypress/tests/standalone/filtering_test.ts @@ -71,8 +71,6 @@ describe('filter', () => { .within(() => cy.get('.dash-cell-value') .then($el => cell_1 = $el[0].innerHTML)); - DashTable.getFilterById('ccc').click(); - DOM.focused.type(`gt`); DashTable.getFilterById('ddd').click(); DOM.focused.type('"20 a000'); DashTable.getFilterById('eee').click(); @@ -85,12 +83,10 @@ describe('filter', () => { DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_1)); DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', '! !"')); - DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt')); DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', '"20 a000')); DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', 'is prime2')); DashTable.getFilterById('bbb').should('have.class', 'invalid'); - DashTable.getFilterById('ccc').should('have.class', 'invalid'); DashTable.getFilterById('ddd').should('have.class', 'invalid'); DashTable.getFilterById('eee').should('have.class', 'invalid'); }); @@ -113,14 +109,43 @@ describe('filter', () => { DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '100')); }); + it('does not use text-based relational operators unless they are followed by a space', () => { + DashTable.getCellById(2, 'ccc').click(); + DOM.focused.type(`le5${Key.Enter}`); + + DashTable.getFilterById('ccc').click(); + DOM.focused.type(`le5${Key.Enter}`); + DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', 'le5')); + DashTable.getCellById(0, 'rows').within(() => cy.get('.dash-cell-value').should('have.html', '3')); + + cy.get('.clear-filters').click(); + + DashTable.getFilterById('ccc').click(); + DOM.focused.type(`le 5${Key.Enter}`); + DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '1')); + DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '2')); + DashTable.getCellById(2, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '4')); + DashTable.getCellById(3, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '5')); + }); + + it('uses symbol relational operators that are not followed by a space', () => { + DashTable.getFilterById('ccc').click(); + DOM.focused.type(`<=5${Key.Enter}`); + DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '1')); + DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '2')); + DashTable.getCellById(2, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '3')); + DashTable.getCellById(3, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '4')); + DashTable.getCellById(4, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '5')); + }); + it('typing invalid followed by valid query fragment does not reset invalid', () => { DashTable.getFilterById('ccc').click(); - DOM.focused.type(`gt`); + DOM.focused.type(`is prime2`); DashTable.getFilterById('ddd').click(); DOM.focused.type('lt 20000'); DashTable.getFilterById('eee').click(); - DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt')); + DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'is prime2')); DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', 'lt 20000')); }); diff --git a/tests/cypress/tests/unit/query_syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts index f99010eea..6cddb8572 100644 --- a/tests/cypress/tests/unit/query_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -589,5 +589,17 @@ describe('Query Syntax Tree', () => { expect(tree.evaluate({ a: 'abc v' })).to.equal(true); expect(tree.evaluate({ a: 'abc w' })).to.equal(false); }); + + it('correctly interprets text-based with no spaces as invalid', () => { + const tree = new QuerySyntaxTree('{a} le5'); + expect(tree.isValid).to.equal(false); + }); + + it('correctly interprets non-text-based with no spaces as valid', () => { + const tree = new QuerySyntaxTree('{a}<=5'); + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 4 })).to.equal(true); + + }); }); -}); \ No newline at end of file +});