diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f301aa700..2111dfacce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Added support for negated or clauses to `EuiSearchBar` ([#2140](https://github.com/elastic/eui/pull/2140)) + **Bug fixes** - Fixed `EuiComboBox`'s padding on the right ([#2135](https://github.com/elastic/eui/pull/2135)) diff --git a/src/components/search_bar/query/ast.js b/src/components/search_bar/query/ast.js index 67a7e3e343a..7f8106fd8f2 100644 --- a/src/components/search_bar/query/ast.js +++ b/src/components/search_bar/query/ast.js @@ -89,6 +89,9 @@ const Group = Object.freeze({ must: value => { return { type: Group.TYPE, value, match: Match.MUST }; }, + mustNot: value => { + return { type: Group.TYPE, value, match: Match.MUST_NOT }; + }, }); const Field = Object.freeze({ diff --git a/src/components/search_bar/query/default_syntax.js b/src/components/search_bar/query/default_syntax.js index 62fe947dd3d..bc12e1e2871 100644 --- a/src/components/search_bar/query/default_syntax.js +++ b/src/components/search_bar/query/default_syntax.js @@ -22,18 +22,22 @@ Clauses } Clause - = IsClause + = GroupClause + / IsClause / FieldClause / TermClause - / GroupClause -GroupClause +SubGroupClause = "(" head:Clause tail:( space? orWord space? clause:Clause { return clause } )* ")" { - return AST.Group.must([head, ...tail]); + return [head, ...tail]; } +GroupClause + = space? "-" group:SubGroupClause { return AST.Group.mustNot(group) } + / space? group:SubGroupClause { return AST.Group.must(group) } + TermClause = space? "-" value:termValue { return AST.Term.mustNot(value); } / space? value:termValue { return AST.Term.must(value); } diff --git a/src/components/search_bar/query/default_syntax.test.js b/src/components/search_bar/query/default_syntax.test.js index c1d7e961d64..fb77e6165c0 100644 --- a/src/components/search_bar/query/default_syntax.test.js +++ b/src/components/search_bar/query/default_syntax.test.js @@ -1083,6 +1083,42 @@ describe('defaultSyntax', () => { expect(nameClauseB.value).toBe('susan'); }); + test('negated OR clause', () => { + const query = '-(name:john OR name:susan)'; + const schema = { + strict: true, + fields: { + name: { + type: 'string', + }, + }, + }; + const ast = defaultSyntax.parse(query, { schema }); + + expect(ast).toBeDefined(); + expect(ast.clauses).toHaveLength(1); + + const groupClauses = ast.getGroupClauses(); + expect(groupClauses).toHaveLength(1); + + const [groupClause] = groupClauses; + expect(groupClause).toBeDefined(); + expect(AST.Group.isInstance(groupClause)).toBe(true); + expect(AST.Match.isMustClause(groupClause)).toBe(false); + + const [nameClauseA, nameClauseB] = groupClause.value; + + expect(AST.Field.isInstance(nameClauseA)).toBe(true); + expect(AST.Match.isMustClause(nameClauseA)).toBe(true); + expect(nameClauseA.field).toBe('name'); + expect(nameClauseA.value).toBe('john'); + + expect(AST.Field.isInstance(nameClauseB)).toBe(true); + expect(AST.Match.isMustClause(nameClauseB)).toBe(true); + expect(nameClauseB.field).toBe('name'); + expect(nameClauseB.value).toBe('susan'); + }); + test('or term parsing and printing', () => { const query = '"or"'; const ast = defaultSyntax.parse(query); diff --git a/src/components/search_bar/query/execute_ast.js b/src/components/search_bar/query/execute_ast.js index 03e6e20aa18..cb47597ab05 100644 --- a/src/components/search_bar/query/execute_ast.js +++ b/src/components/search_bar/query/execute_ast.js @@ -148,7 +148,7 @@ export const createFilter = ( } throw new Error(`Unknown query clause type in group, [${clause.type}]`); }); - return matchesGroup; + return AST.Match.isMustClause(clause) ? matchesGroup : !matchesGroup; }); return isGroupMatch; diff --git a/src/components/search_bar/query/execute_ast.test.js b/src/components/search_bar/query/execute_ast.test.js index f448d540206..5be44c2d11d 100644 --- a/src/components/search_bar/query/execute_ast.test.js +++ b/src/components/search_bar/query/execute_ast.test.js @@ -365,6 +365,27 @@ describe('execute ast', () => { expect(names).toContain('foo bar'); }); + test('negated OR clause', () => { + const items = [ + { name: 'john doe', age: 9 }, + { name: 'foo', age: 6 }, + { name: 'foo bar', age: 7 }, + { name: 'bar', age: 8 }, + ]; + const result = executeAst( + AST.create([ + AST.Group.mustNot([ + AST.Field.must.eq('name', 'john doe'), + AST.Field.must.eq('name', 'foo'), + ]), + ]), + items + ); + expect(result).toHaveLength(1); + const names = result.map(item => item.name); + expect(names).toContain('bar'); + }); + describe('array field values', () => { describe('eq operator', () => { test('full match', () => {