From 463a85465c476128330239caf83b9c35925b31a5 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 7 Sep 2023 17:07:55 -0700 Subject: [PATCH 1/8] [setup] move unused `SchemaType` export to main `EuiSearchBar` - not clear why it lives in `EuiSearchBox`, which doesn't reference it whatsoever + clean up/combine `EuiInMemoryTable` imports --- src/components/basic_table/in_memory_table.tsx | 10 +++++++--- src/components/search_bar/search_bar.tsx | 8 +++++++- src/components/search_bar/search_box.tsx | 7 +------ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index dd606fd58d2..3f0bcd72ce0 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -23,11 +23,15 @@ import { PropertySort } from '../../services'; import { Pagination as PaginationBarType } from './pagination_bar'; import { isString } from '../../services/predicate'; import { Comparators, Direction } from '../../services/sort'; -import { EuiSearchBar, Query } from '../search_bar'; +import { + EuiSearchBar, + EuiSearchBarProps, + Query, + SchemaType, +} from '../search_bar/search_bar'; +import { EuiSearchBox } from '../search_bar/search_box'; import { EuiSpacer } from '../spacer'; import { CommonProps } from '../common'; -import { EuiSearchBarProps } from '../search_bar/search_bar'; -import { SchemaType } from '../search_bar/search_box'; import { EuiTablePaginationProps, euiTablePaginationDefaults, diff --git a/src/components/search_bar/search_bar.tsx b/src/components/search_bar/search_bar.tsx index a91f60c8b54..783440d90f0 100644 --- a/src/components/search_bar/search_bar.tsx +++ b/src/components/search_bar/search_bar.tsx @@ -11,7 +11,7 @@ import React, { Component, ReactElement } from 'react'; import { htmlIdGenerator } from '../../services/accessibility'; import { isString } from '../../services/predicate'; import { EuiFlexGroup, EuiFlexItem } from '../flex'; -import { EuiSearchBox, SchemaType } from './search_box'; +import { EuiSearchBox } from './search_box'; import { EuiSearchBarFilters, SearchFilterConfig } from './search_filters'; import { Query } from './query'; import { CommonProps } from '../common'; @@ -36,6 +36,12 @@ interface ArgsWithError { error: Error; } +export interface SchemaType { + strict?: boolean; + fields?: any; + flags?: string[]; +} + export type EuiSearchBarOnChangeArgs = ArgsWithQuery | ArgsWithError; type HintPopOverProps = Partial< diff --git a/src/components/search_bar/search_box.tsx b/src/components/search_bar/search_box.tsx index 85a78d3a726..2c417d93b2b 100644 --- a/src/components/search_bar/search_box.tsx +++ b/src/components/search_bar/search_box.tsx @@ -9,13 +9,8 @@ import React, { Component } from 'react'; import { EuiFieldSearch, EuiFieldSearchProps } from '../form'; import { EuiInputPopover } from '../popover'; -import { EuiSearchBarProps } from './search_bar'; -export interface SchemaType { - strict?: boolean; - fields?: any; - flags?: string[]; -} +import { EuiSearchBarProps } from './search_bar'; export interface EuiSearchBoxProps extends EuiFieldSearchProps { query: string; From ff7f6ed69e1a00993209f0316b70d6cbd30e4172 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 7 Sep 2023 17:27:54 -0700 Subject: [PATCH 2/8] [tech debt] Convert `EuiSearchBox` to function component + i18n text --- src/components/search_bar/search_box.tsx | 131 ++++++++++++----------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/src/components/search_bar/search_box.tsx b/src/components/search_bar/search_box.tsx index 2c417d93b2b..403e9840da3 100644 --- a/src/components/search_bar/search_box.tsx +++ b/src/components/search_bar/search_box.tsx @@ -6,7 +6,10 @@ * Side Public License, v 1. */ -import React, { Component } from 'react'; +import React, { FunctionComponent, useRef } from 'react'; + +import { useUpdateEffect } from '../../services'; +import { useEuiI18n } from '../i18n'; import { EuiFieldSearch, EuiFieldSearchProps } from '../form'; import { EuiInputPopover } from '../popover'; @@ -16,6 +19,10 @@ export interface EuiSearchBoxProps extends EuiFieldSearchProps { query: string; // This is optional in EuiFieldSearchProps onSearch: (queryText: string) => void; + /** + * @default Search... + */ + placeholder?: string; hint?: { id: string; isVisible: boolean; @@ -23,73 +30,73 @@ export interface EuiSearchBoxProps extends EuiFieldSearchProps { } & EuiSearchBarProps['hint']; } -type DefaultProps = Pick; - -export class EuiSearchBox extends Component { - static defaultProps: DefaultProps = { - placeholder: 'Search...', - incremental: false, - }; - - private inputElement: HTMLInputElement | null = null; +export const EuiSearchBox: FunctionComponent = ({ + query, + placeholder, + incremental, + hint, + ...rest +}) => { + const inputRef = useRef(null); - componentDidUpdate(oldProps: EuiSearchBoxProps) { - if (oldProps.query !== this.props.query && this.inputElement != null) { - this.inputElement.value = this.props.query; - this.inputElement.dispatchEvent(new Event('change')); + useUpdateEffect(() => { + if (inputRef.current) { + inputRef.current.value = query; + inputRef.current.dispatchEvent(new Event('change')); } - } + }, [query]); - render() { - const { query, incremental, hint, ...rest } = this.props; + const defaultPlaceholder = useEuiI18n( + 'euiSearchBox.placeholder', + 'Search...' + ); + const ariaLabelIncremental = useEuiI18n( + 'euiSearchBox.incrementalAriaLabel', + 'This is a search bar. As you type, the results lower in the page will automatically filter.' + ); + const ariaLabelEnter = useEuiI18n( + 'euiSearchBox.ariaLabel', + 'This is a search bar. After typing your query, hit enter to filter the results lower in the page.' + ); - let ariaLabel; - if (incremental) { - ariaLabel = - 'This is a search bar. As you type, the results lower in the page will automatically filter.'; - } else { - ariaLabel = - 'This is a search bar. After typing your query, hit enter to filter the results lower in the page.'; - } + const search = ( + (inputRef.current = input)} + fullWidth + defaultValue={query} + incremental={incremental} + aria-label={incremental ? ariaLabelIncremental : ariaLabelEnter} + placeholder={placeholder ?? defaultPlaceholder} + onFocus={() => { + hint?.setIsVisible(true); + }} + {...rest} + /> + ); - const search = ( - (this.inputElement = input)} + if (hint) { + return ( + { - hint?.setIsVisible(true); + closePopover={() => { + hint.setIsVisible(false); + }} + panelProps={{ + 'aria-live': undefined, + 'aria-modal': undefined, + role: undefined, + tabIndex: -1, + id: hint.id, }} - {...rest} - /> + {...hint.popoverProps} + > + {hint.content} + ); - - if (hint) { - return ( - { - hint.setIsVisible(false); - }} - panelProps={{ - 'aria-live': undefined, - 'aria-modal': undefined, - role: undefined, - tabIndex: -1, - id: hint.id, - }} - {...hint.popoverProps} - > - {hint.content} - - ); - } - - return search; } -} + + return search; +}; From 65e52051f9b15fc5fcb39e11ae1bcd9a6e8cf560 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Fri, 8 Sep 2023 17:28:37 -0700 Subject: [PATCH 3/8] Add new `plainTextSearch` prop that allows consumers to treat user searchas non-EQL searches --- .../basic_table/in_memory_table.test.tsx | 29 +++++++++++ .../basic_table/in_memory_table.tsx | 49 ++++++++++++++++--- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx index 96a46d1d607..38e9bf260e5 100644 --- a/src/components/basic_table/in_memory_table.test.tsx +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -1437,4 +1437,33 @@ describe('EuiInMemoryTable', () => { expect(tableContent.at(2).text()).toBe('baz'); }); }); + + describe('plain text search', () => { + it('allows searching for any text with special characters in it', () => { + const specialCharacterSearch = '!@#$%^&*(){}+=-_hello:world"`<>?/👋~.,;|'; + const items = [ + { title: specialCharacterSearch }, + { title: 'no special characters' }, + ]; + const columns = [{ field: 'title', name: 'Title' }]; + + const { getByTestSubject, container } = render( + + ); + fireEvent.keyUp(getByTestSubject('searchbox'), { + target: { value: specialCharacterSearch }, + }); + + const tableContent = container.querySelectorAll( + '.euiTableRowCell .euiTableCellContent' + ); + expect(tableContent).toHaveLength(1); // only 1 match + expect(tableContent[0]).toHaveTextContent(specialCharacterSearch); + }); + }); }); diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index 3f0bcd72ce0..85cb60f61e8 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -80,6 +80,15 @@ type InMemoryTableProps = Omit< * Configures #Search. */ search?: Search; + /** + * If passed as true, search ignores all filters and EQL syntax, and anything + * typed into the table search bar is treated as plain text. + * + * This functionality allows users to search for strings with special characters + * such as quotes, parentheses, and colons, which are normally otherwise + * reserved for EQL syntax. + */ + searchPlainText?: boolean; /** * Configures #Pagination */ @@ -525,9 +534,31 @@ export class EuiInMemoryTable extends Component< })); }; + // Alternative to onQueryChange - allows consumers to specify they want the + // search bar to ignore EQL syntax and only use the searchbar for plain text + onPlainTextSearch = (searchValue: string) => { + const escapedQueryText = searchValue.replaceAll('"', '\\"'); + const finalQuery = `"${escapedQueryText}"`; + this.setState({ + query: EuiSearchBar.Query.parse(finalQuery), + }); + }; + renderSearchBar() { - const { search } = this.props; - if (search) { + const { search, searchPlainText } = this.props; + if (!search) return; + + let searchBar: ReactNode; + + if (searchPlainText) { + searchBar = ( + + ); + } else { let searchBarProps: Omit = {}; if (isEuiSearchBarProps(search)) { @@ -542,13 +573,17 @@ export class EuiInMemoryTable extends Component< } } - return ( - <> - - - + searchBar = ( + ); } + + return ( + <> + {searchBar} + + + ); } resolveSearchSchema(): SchemaType { From bf1235ebc25b363c6ff495fbb2cd0a9c6772b4fb Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Fri, 8 Sep 2023 17:29:29 -0700 Subject: [PATCH 4/8] Fix test errors w/ non-HTML attributes being passed via `...rest` --- src/components/basic_table/in_memory_table.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index 85cb60f61e8..c1cb7e3150a 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -551,10 +551,13 @@ export class EuiInMemoryTable extends Component< let searchBar: ReactNode; if (searchPlainText) { + const _searchBoxProps = (search as EuiSearchBarProps)?.box || {}; // Work around | boolean type + const { schema, ...searchBoxProps } = _searchBoxProps; // Destructure `schema` so it doesn't get rendered to DOM + searchBar = ( ); @@ -692,6 +695,7 @@ export class EuiInMemoryTable extends Component< tableLayout, items: _unuseditems, search, + searchPlainText, onTableChange, executeQueryOptions, allowNeutralSort, From 74f96cc9225c4a5cf805a627a9373a548f66e07d Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Fri, 8 Sep 2023 17:31:03 -0700 Subject: [PATCH 5/8] Add documentation toggle for searching plain text/special characters + update faker.js to latest to get string.symbol API --- package.json | 2 +- .../tables/in_memory/in_memory_search.tsx | 61 +++++++++++-------- yarn.lock | 8 +-- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index f136e9f725f..6d4e8635abc 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "@emotion/eslint-plugin": "^11.11.0", "@emotion/jest": "^11.11.0", "@emotion/react": "^11.11.0", - "@faker-js/faker": "^7.6.0", + "@faker-js/faker": "^8.0.2", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", "@storybook/addon-essentials": "^7.3.1", "@storybook/addon-interactions": "^7.3.1", diff --git a/src-docs/src/views/tables/in_memory/in_memory_search.tsx b/src-docs/src/views/tables/in_memory/in_memory_search.tsx index 7d27f0e5c3a..cc6d95bdfca 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_search.tsx +++ b/src-docs/src/views/tables/in_memory/in_memory_search.tsx @@ -11,7 +11,6 @@ import { EuiSpacer, EuiSwitch, EuiFlexGroup, - EuiFlexItem, EuiCallOut, EuiCode, } from '../../../../../src/components'; @@ -27,16 +26,23 @@ type User = { }; const users: User[] = []; +const usersWithSpecialCharacters: User[] = []; for (let i = 0; i < 20; i++) { - users.push({ + const userData = { id: i + 1, - firstName: faker.name.firstName(), - lastName: faker.name.lastName(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), github: faker.internet.userName(), dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), - location: faker.address.country(), + location: faker.location.country(), + }; + users.push(userData); + usersWithSpecialCharacters.push({ + ...userData, + firstName: `${userData.firstName} "${faker.string.symbol(10)}"`, + lastName: `${userData.lastName} ${faker.internet.emoji()}`, }); } @@ -108,6 +114,7 @@ export default () => { const [incremental, setIncremental] = useState(false); const [filters, setFilters] = useState(false); const [contentBetween, setContentBetween] = useState(false); + const [searchPlainText, setSearchPlainText] = useState(false); const search: EuiSearchBarProps = { box: { @@ -138,34 +145,34 @@ export default () => { return ( <> - - setIncremental(!incremental)} - /> - - - setFilters(!filters)} - /> - - - setContentBetween(!contentBetween)} - /> - + setIncremental(!incremental)} + /> + setFilters(!filters)} + /> + setContentBetween(!contentBetween)} + /> + setSearchPlainText(!searchPlainText)} + /> Date: Fri, 8 Sep 2023 17:37:54 -0700 Subject: [PATCH 6/8] changelog --- upcoming_changelogs/7175.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 upcoming_changelogs/7175.md diff --git a/upcoming_changelogs/7175.md b/upcoming_changelogs/7175.md new file mode 100644 index 00000000000..ddda1327451 --- /dev/null +++ b/upcoming_changelogs/7175.md @@ -0,0 +1,5 @@ +- Updated `EuiInMemoryTable` with a new `searchPlainText` prop. This boolean flag allows the built-in search bar to ignore EQL syntax and search for plain strings with special characters and symbols. + +**Bug fixes** + +- Fixed missing i18n in `EuiSearchBar`'s default placeholder and aria-label text From feb7a9117cc588a4d3917a1ddfda02fe6bf55d1f Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 14 Sep 2023 18:20:46 -0700 Subject: [PATCH 7/8] [PR feedback] Change `searchPlainText` prop name to `searchFormat` --- .../tables/in_memory/in_memory_search.tsx | 10 ++++----- .../basic_table/in_memory_table.test.tsx | 4 ++-- .../basic_table/in_memory_table.tsx | 22 +++++++++++-------- upcoming_changelogs/7175.md | 2 +- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src-docs/src/views/tables/in_memory/in_memory_search.tsx b/src-docs/src/views/tables/in_memory/in_memory_search.tsx index cc6d95bdfca..f92b3558736 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_search.tsx +++ b/src-docs/src/views/tables/in_memory/in_memory_search.tsx @@ -114,7 +114,7 @@ export default () => { const [incremental, setIncremental] = useState(false); const [filters, setFilters] = useState(false); const [contentBetween, setContentBetween] = useState(false); - const [searchPlainText, setSearchPlainText] = useState(false); + const [textSearchFormat, setTextSearchFormat] = useState(false); const search: EuiSearchBarProps = { box: { @@ -162,17 +162,17 @@ export default () => { /> setSearchPlainText(!searchPlainText)} + checked={textSearchFormat} + onChange={() => setTextSearchFormat(!textSearchFormat)} /> { }); }); - describe('plain text search', () => { + describe('text search format', () => { it('allows searching for any text with special characters in it', () => { const specialCharacterSearch = '!@#$%^&*(){}+=-_hello:world"`<>?/👋~.,;|'; const items = [ @@ -1450,7 +1450,7 @@ describe('EuiInMemoryTable', () => { const { getByTestSubject, container } = render( diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index c1cb7e3150a..6fc5dbfce56 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -81,14 +81,17 @@ type InMemoryTableProps = Omit< */ search?: Search; /** - * If passed as true, search ignores all filters and EQL syntax, and anything - * typed into the table search bar is treated as plain text. + * By default, tables use `eql` format for search which allows using advanced filters. * - * This functionality allows users to search for strings with special characters - * such as quotes, parentheses, and colons, which are normally otherwise - * reserved for EQL syntax. + * However, certain special characters (such as quotes, parentheses, and colons) + * are reserved for EQL syntax and will error if used. + * If your table does not require filter search and instead requires searching for certain + * symbols, use a plain `text` search format instead (note that filters will be ignored + * in this format). + * + * @default "eql" */ - searchPlainText?: boolean; + searchFormat?: 'eql' | 'text'; /** * Configures #Pagination */ @@ -298,6 +301,7 @@ export class EuiInMemoryTable extends Component< static defaultProps = { responsive: true, tableLayout: 'fixed', + searchFormat: 'eql', }; tableRef: React.RefObject; @@ -545,12 +549,12 @@ export class EuiInMemoryTable extends Component< }; renderSearchBar() { - const { search, searchPlainText } = this.props; + const { search, searchFormat } = this.props; if (!search) return; let searchBar: ReactNode; - if (searchPlainText) { + if (searchFormat === 'text') { const _searchBoxProps = (search as EuiSearchBarProps)?.box || {}; // Work around | boolean type const { schema, ...searchBoxProps } = _searchBoxProps; // Destructure `schema` so it doesn't get rendered to DOM @@ -695,7 +699,7 @@ export class EuiInMemoryTable extends Component< tableLayout, items: _unuseditems, search, - searchPlainText, + searchFormat, onTableChange, executeQueryOptions, allowNeutralSort, diff --git a/upcoming_changelogs/7175.md b/upcoming_changelogs/7175.md index ddda1327451..b570f6c3d93 100644 --- a/upcoming_changelogs/7175.md +++ b/upcoming_changelogs/7175.md @@ -1,4 +1,4 @@ -- Updated `EuiInMemoryTable` with a new `searchPlainText` prop. This boolean flag allows the built-in search bar to ignore EQL syntax and search for plain strings with special characters and symbols. +- Updated `EuiInMemoryTable` with a new `searchFormat` prop (defaults to `eql`). When setting this prop to `text`, the built-in search bar will ignore EQL syntax and allow searching for plain strings with special characters and symbols. **Bug fixes** From 40d2ea3450f13a523c7e389ff30fff1cbce87ebe Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 14 Sep 2023 18:29:03 -0700 Subject: [PATCH 8/8] Fix broken backslash search :tada: --- src/components/basic_table/in_memory_table.test.tsx | 3 ++- src/components/basic_table/in_memory_table.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx index c83244520e4..e15294c9eb4 100644 --- a/src/components/basic_table/in_memory_table.test.tsx +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -1440,7 +1440,8 @@ describe('EuiInMemoryTable', () => { describe('text search format', () => { it('allows searching for any text with special characters in it', () => { - const specialCharacterSearch = '!@#$%^&*(){}+=-_hello:world"`<>?/👋~.,;|'; + const specialCharacterSearch = + '!@#$%^&*(){}+=-_hello:world"`<>?/👋~.,;|\\'; const items = [ { title: specialCharacterSearch }, { title: 'no special characters' }, diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index 6fc5dbfce56..e146bcfe704 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -541,7 +541,7 @@ export class EuiInMemoryTable extends Component< // Alternative to onQueryChange - allows consumers to specify they want the // search bar to ignore EQL syntax and only use the searchbar for plain text onPlainTextSearch = (searchValue: string) => { - const escapedQueryText = searchValue.replaceAll('"', '\\"'); + const escapedQueryText = searchValue.replace(/["\\]/g, '\\$&'); const finalQuery = `"${escapedQueryText}"`; this.setState({ query: EuiSearchBar.Query.parse(finalQuery),