Skip to content

Commit

Permalink
[EuiInMemoryTable] Allow consumers to use non-EQL plain text search w…
Browse files Browse the repository at this point in the history
…ith special characters (#7175)
  • Loading branch information
cee-chen authored Sep 18, 2023
1 parent d1b54f6 commit 0c6ccb0
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 111 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
61 changes: 34 additions & 27 deletions src-docs/src/views/tables/in_memory/in_memory_search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
EuiSpacer,
EuiSwitch,
EuiFlexGroup,
EuiFlexItem,
EuiCallOut,
EuiCode,
} from '../../../../../src/components';
Expand All @@ -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()}`,
});
}

Expand Down Expand Up @@ -108,6 +114,7 @@ export default () => {
const [incremental, setIncremental] = useState(false);
const [filters, setFilters] = useState(false);
const [contentBetween, setContentBetween] = useState(false);
const [textSearchFormat, setTextSearchFormat] = useState(false);

const search: EuiSearchBarProps = {
box: {
Expand Down Expand Up @@ -138,34 +145,34 @@ export default () => {
return (
<>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSwitch
label="Incremental"
checked={incremental}
onChange={() => setIncremental(!incremental)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
label="With Filters"
checked={filters}
onChange={() => setFilters(!filters)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
label="Content between"
checked={contentBetween}
onChange={() => setContentBetween(!contentBetween)}
/>
</EuiFlexItem>
<EuiSwitch
label="Incremental"
checked={incremental}
onChange={() => setIncremental(!incremental)}
/>
<EuiSwitch
label="With Filters"
checked={filters}
onChange={() => setFilters(!filters)}
/>
<EuiSwitch
label="Content between"
checked={contentBetween}
onChange={() => setContentBetween(!contentBetween)}
/>
<EuiSwitch
label="Plain text search"
checked={textSearchFormat}
onChange={() => setTextSearchFormat(!textSearchFormat)}
/>
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiInMemoryTable
tableCaption="Demo of EuiInMemoryTable with search"
items={users}
items={textSearchFormat ? usersWithSpecialCharacters : users}
columns={columns}
search={search}
searchFormat={textSearchFormat ? 'text' : 'eql'}
pagination={true}
sorting={true}
childrenBetween={
Expand Down
30 changes: 30 additions & 0 deletions src/components/basic_table/in_memory_table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1437,4 +1437,34 @@ describe('EuiInMemoryTable', () => {
expect(tableContent.at(2).text()).toBe('baz');
});
});

describe('text search format', () => {
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(
<EuiInMemoryTable
items={items}
searchFormat="text"
search={{ box: { incremental: true, 'data-test-subj': 'searchbox' } }}
columns={columns}
/>
);
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);
});
});
});
67 changes: 57 additions & 10 deletions src/components/basic_table/in_memory_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -76,6 +80,18 @@ type InMemoryTableProps<T> = Omit<
* Configures #Search.
*/
search?: Search;
/**
* By default, tables use `eql` format for search which allows using advanced filters.
*
* 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"
*/
searchFormat?: 'eql' | 'text';
/**
* Configures #Pagination
*/
Expand Down Expand Up @@ -285,6 +301,7 @@ export class EuiInMemoryTable<T> extends Component<
static defaultProps = {
responsive: true,
tableLayout: 'fixed',
searchFormat: 'eql',
};
tableRef: React.RefObject<EuiBasicTable>;

Expand Down Expand Up @@ -521,9 +538,34 @@ export class EuiInMemoryTable<T> 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.replace(/["\\]/g, '\\$&');
const finalQuery = `"${escapedQueryText}"`;
this.setState({
query: EuiSearchBar.Query.parse(finalQuery),
});
};

renderSearchBar() {
const { search } = this.props;
if (search) {
const { search, searchFormat } = this.props;
if (!search) return;

let searchBar: ReactNode;

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

searchBar = (
<EuiSearchBox
query="" // Unused, passed to satisfy Typescript
{...searchBoxProps}
onSearch={this.onPlainTextSearch}
/>
);
} else {
let searchBarProps: Omit<EuiSearchBarProps, 'onChange'> = {};

if (isEuiSearchBarProps(search)) {
Expand All @@ -538,13 +580,17 @@ export class EuiInMemoryTable<T> extends Component<
}
}

return (
<>
<EuiSearchBar onChange={this.onQueryChange} {...searchBarProps} />
<EuiSpacer size="l" />
</>
searchBar = (
<EuiSearchBar onChange={this.onQueryChange} {...searchBarProps} />
);
}

return (
<>
{searchBar}
<EuiSpacer size="l" />
</>
);
}

resolveSearchSchema(): SchemaType {
Expand Down Expand Up @@ -653,6 +699,7 @@ export class EuiInMemoryTable<T> extends Component<
tableLayout,
items: _unuseditems,
search,
searchFormat,
onTableChange,
executeQueryOptions,
allowNeutralSort,
Expand Down
8 changes: 7 additions & 1 deletion src/components/search_bar/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<
Expand Down
Loading

0 comments on commit 0c6ccb0

Please sign in to comment.