Skip to content

Commit 5a429b4

Browse files
authored
[EuiInMemoryTable] Support controlled plain text search (#9142)
1 parent 531a048 commit 5a429b4

File tree

4 files changed

+190
-28
lines changed

4 files changed

+190
-28
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**Bug fixes**
2+
3+
- Fixed `EuiInMemoryTable` support for controlled search for plain text (when `searchFormat="text"`) by properly handling `search.query` and `search.defaulQuery`

packages/eui/src/components/basic_table/in_memory_table.test.tsx

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1533,7 +1533,7 @@ describe('EuiInMemoryTable', () => {
15331533
);
15341534
expect(mockOnChange).toHaveBeenCalledTimes(1);
15351535
expect(mockOnChange).toHaveBeenCalledWith({
1536-
query: Query.parse(`"${TEXT}"`),
1536+
query: null,
15371537
queryText: TEXT,
15381538
error: null,
15391539
});
@@ -1569,5 +1569,108 @@ describe('EuiInMemoryTable', () => {
15691569
);
15701570
expect(tableContent).toHaveLength(2); // 2 matches for "ba"
15711571
});
1572+
1573+
it('supports controlled operation, query is reflected in the search box', () => {
1574+
const items = [{ title: 'foo' }, { title: 'bar' }, { title: 'baz' }];
1575+
const columns = [{ field: 'title', name: 'Title' }];
1576+
const CONTROLLED_QUERY = 'controlled search';
1577+
1578+
const { getByTestSubject, rerender } = render(
1579+
<EuiInMemoryTable
1580+
items={items}
1581+
searchFormat="text"
1582+
search={{
1583+
query: CONTROLLED_QUERY,
1584+
box: { incremental: true, 'data-test-subj': 'searchbox' },
1585+
}}
1586+
columns={columns}
1587+
/>
1588+
);
1589+
1590+
const searchbox = getByTestSubject('searchbox') as HTMLInputElement;
1591+
expect(searchbox.value).toBe(CONTROLLED_QUERY);
1592+
1593+
// Update the controlled query
1594+
const UPDATED_QUERY = 'updated search';
1595+
rerender(
1596+
<EuiInMemoryTable
1597+
items={items}
1598+
searchFormat="text"
1599+
search={{
1600+
query: UPDATED_QUERY,
1601+
box: { incremental: true, 'data-test-subj': 'searchbox' },
1602+
}}
1603+
columns={columns}
1604+
/>
1605+
);
1606+
1607+
expect(searchbox.value).toBe(UPDATED_QUERY);
1608+
});
1609+
1610+
it('renders defaultQuery in the search box on initial render', () => {
1611+
const items = [{ title: 'foo' }, { title: 'bar' }, { title: 'baz' }];
1612+
const columns = [{ field: 'title', name: 'Title' }];
1613+
const DEFAULT_QUERY = 'default search text';
1614+
1615+
const { getByTestSubject } = render(
1616+
<EuiInMemoryTable
1617+
items={items}
1618+
searchFormat="text"
1619+
search={{
1620+
defaultQuery: DEFAULT_QUERY,
1621+
box: { incremental: true, 'data-test-subj': 'searchbox' },
1622+
}}
1623+
columns={columns}
1624+
/>
1625+
);
1626+
1627+
const searchbox = getByTestSubject('searchbox') as HTMLInputElement;
1628+
1629+
expect(searchbox.value).toBe(DEFAULT_QUERY);
1630+
1631+
fireEvent.keyUp(searchbox, {
1632+
target: { value: 'something else' },
1633+
});
1634+
1635+
expect(searchbox.value).toBe('something else');
1636+
});
1637+
1638+
it('ignores Query objects in the search box', () => {
1639+
const items = [{ title: 'foo' }, { title: 'bar' }, { title: 'baz' }];
1640+
const columns = [{ field: 'title', name: 'Title' }];
1641+
const query = Query.parse('ba');
1642+
1643+
const { getByTestSubject, rerender } = render(
1644+
<EuiInMemoryTable
1645+
items={items}
1646+
searchFormat="text"
1647+
search={{
1648+
query,
1649+
box: { incremental: true, 'data-test-subj': 'searchbox' },
1650+
}}
1651+
columns={columns}
1652+
/>
1653+
);
1654+
1655+
const searchbox = getByTestSubject('searchbox') as HTMLInputElement;
1656+
1657+
// not `[object Object]`
1658+
expect(searchbox.value).toBe('');
1659+
1660+
rerender(
1661+
<EuiInMemoryTable
1662+
items={items}
1663+
searchFormat="text"
1664+
search={{
1665+
defaultQuery: query,
1666+
box: { incremental: true, 'data-test-subj': 'searchbox' },
1667+
}}
1668+
columns={columns}
1669+
/>
1670+
);
1671+
1672+
// not `[object Object]`
1673+
expect(searchbox.value).toBe('');
1674+
});
15721675
});
15731676
});

packages/eui/src/components/basic_table/in_memory_table.tsx

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,19 @@ type InMemoryTableProps<T extends object> = Omit<
8080
*/
8181
noItemsMessage?: ReactNode;
8282
/**
83-
* Configures {@link Search}.
83+
* Configures the search bar. Can be `true` for defaults,
84+
* or an {@link EuiSearchBarProps} object.
85+
*
86+
* When `searchFormat="text"`, `query` and `defaultQuery` must be strings
87+
* ({@link Query} objects are ignored).
8488
*/
8589
search?: Search;
8690
/**
8791
* By default, tables use `eql` format for search which allows using advanced filters.
8892
*
8993
* However, certain special characters (such as quotes, parentheses, and colons)
9094
* are reserved for EQL syntax and will error if used.
95+
*
9196
* If your table does not require filter search and instead requires searching for certain
9297
* symbols, use a plain `text` search format instead (note that filters will be ignored
9398
* in this format).
@@ -154,7 +159,7 @@ interface State<T extends object> {
154159
search?: Search;
155160
};
156161
search?: Search;
157-
query: Query | null;
162+
query: Query | string | null;
158163
pageIndex: number;
159164
pageSize?: number;
160165
pageSizeOptions?: number[];
@@ -164,23 +169,34 @@ interface State<T extends object> {
164169
showPerPageOptions: boolean | undefined;
165170
}
166171

172+
/**
173+
* Extracts and formats a query from search props based on the search format
174+
* @param search - The search configuration
175+
* @param defaultQuery - Whether to use the defaultQuery property as fallback
176+
* @param searchFormat - The search format: 'eql' for parsed queries, 'text' for plain text
177+
* @returns Formatted query string or Query object
178+
*/
167179
const getQueryFromSearch = (
168180
search: Search | undefined,
169-
defaultQuery: boolean
170-
) => {
171-
let query: Query | string;
181+
defaultQuery: boolean,
182+
searchFormat: InMemoryTableProps<{}>['searchFormat']
183+
): Query | string => {
172184
if (!search) {
173-
query = '';
174-
} else {
175-
query =
176-
(defaultQuery
177-
? (search as EuiSearchBarProps).defaultQuery ||
178-
(search as EuiSearchBarProps).query ||
179-
''
180-
: (search as EuiSearchBarProps).query) || '';
185+
return searchFormat === 'text' ? '""' : '';
186+
}
187+
188+
const searchProps = search as EuiSearchBarProps;
189+
const queryString = defaultQuery
190+
? searchProps.defaultQuery ?? searchProps.query ?? ''
191+
: searchProps.query ?? '';
192+
193+
if (searchFormat === 'text') {
194+
return `"${queryString}"`;
181195
}
182196

183-
return isString(query) ? EuiSearchBar.Query.parse(query) : query;
197+
return isString(queryString)
198+
? EuiSearchBar.Query.parse(queryString)
199+
: queryString;
184200
};
185201

186202
const getInitialPagination = (
@@ -393,7 +409,11 @@ export class EuiInMemoryTable<T extends object = object> extends Component<
393409
...updatedPrevState.prevProps,
394410
search: nextProps.search,
395411
},
396-
query: getQueryFromSearch(nextProps.search, false),
412+
query: getQueryFromSearch(
413+
nextProps.search,
414+
false,
415+
nextProps.searchFormat ?? 'eql'
416+
),
397417
};
398418
}
399419
if (updatedPrevState !== prevState) {
@@ -418,7 +438,7 @@ export class EuiInMemoryTable<T extends object = object> extends Component<
418438
search,
419439
},
420440
search: search,
421-
query: getQueryFromSearch(search, true),
441+
query: getQueryFromSearch(search, true, props.searchFormat ?? 'eql'),
422442
pageIndex: pageIndex || 0,
423443
pageSize,
424444
pageSizeOptions,
@@ -537,13 +557,12 @@ export class EuiInMemoryTable<T extends object = object> extends Component<
537557
// search bar to ignore EQL syntax and only use the searchbar for plain text
538558
onPlainTextSearch = (searchValue: string) => {
539559
const escapedQueryText = searchValue.replace(/["\\]/g, '\\$&');
540-
const finalQuery = `"${escapedQueryText}"`;
541560
const { search } = this.props;
542561

543562
if (isEuiSearchBarProps(search)) {
544563
if (search.onChange) {
545564
const shouldQueryInMemory = search.onChange({
546-
query: EuiSearchBar.Query.parse(finalQuery),
565+
query: null,
547566
queryText: escapedQueryText,
548567
error: null,
549568
});
@@ -554,7 +573,7 @@ export class EuiInMemoryTable<T extends object = object> extends Component<
554573
}
555574

556575
this.setState({
557-
query: EuiSearchBar.Query.parse(finalQuery),
576+
query: `"${escapedQueryText}"`,
558577
});
559578
};
560579

@@ -565,13 +584,37 @@ export class EuiInMemoryTable<T extends object = object> extends Component<
565584
let searchBar: ReactNode;
566585

567586
if (searchFormat === 'text') {
568-
const _searchBoxProps = (search as EuiSearchBarProps)?.box || {}; // Work around | boolean type
569-
const { schema, ...searchBoxProps } = _searchBoxProps; // Destructure `schema` so it doesn't get rendered to DOM
587+
const { box = {}, query, defaultQuery } = search as EuiSearchBarProps;
588+
const {
589+
schema, // destructure `schema` so it doesn't get rendered to DOM
590+
...searchBoxProps
591+
} = box;
592+
593+
// in the unexpected case a Query object is passed with searchFormat=text
594+
if (process.env.NODE_ENV === 'development') {
595+
if (query != null && !isString(query)) {
596+
console.warn(
597+
'EuiInMemoryTable: `query` should be a string when using searchFormat="text". Query objects are only supported with searchFormat="eql".'
598+
);
599+
}
600+
if (defaultQuery != null && !isString(defaultQuery)) {
601+
console.warn(
602+
'EuiInMemoryTable: `defaultQuery` should be a string when using searchFormat="text". Query objects are only supported with searchFormat="eql".'
603+
);
604+
}
605+
}
606+
607+
// use only string values, ignore Query objects
608+
const displayQuery = isString(query)
609+
? query
610+
: isString(defaultQuery)
611+
? defaultQuery
612+
: '';
570613

571614
searchBar = (
572615
<EuiSearchBox
573-
query="" // Unused, passed to satisfy Typescript
574616
{...searchBoxProps}
617+
query={displayQuery}
575618
onSearch={this.onPlainTextSearch}
576619
/>
577620
);

packages/eui/src/components/search_bar/search_bar.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ interface ArgsWithQuery {
3434
error: null;
3535
}
3636

37+
/**
38+
* When `searchFormat` is 'text', `query` is null and the search is performed
39+
* on the `queryText` directly without EQL parsing
40+
*/
41+
interface ArgsWithPlainText {
42+
query: null;
43+
queryText: string;
44+
error: null;
45+
}
46+
3747
interface ArgsWithError {
3848
query: null;
3949
queryText: string;
@@ -48,7 +58,10 @@ export interface SchemaType {
4858
recognizedFields?: string[];
4959
}
5060

51-
export type EuiSearchBarOnChangeArgs = ArgsWithQuery | ArgsWithError;
61+
export type EuiSearchBarOnChangeArgs =
62+
| ArgsWithQuery
63+
| ArgsWithPlainText
64+
| ArgsWithError;
5265

5366
type HintPopOverProps = Partial<
5467
Pick<
@@ -72,17 +85,17 @@ export interface EuiSearchBarProps extends CommonProps {
7285
onChange?: (args: EuiSearchBarOnChangeArgs) => void | boolean;
7386

7487
/**
75-
The initial query the bar will hold when first mounted
88+
* The initial query the bar will hold when first mounted
7689
*/
7790
defaultQuery?: QueryType;
7891

7992
/**
80-
If you wish to use the search bar as a controlled component, continuously pass the query via this prop.
93+
* If you wish to use the search bar as a controlled component, continuously pass the query via this prop.
8194
*/
8295
query?: QueryType;
8396

8497
/**
85-
Configures the search box. Set `placeholder` to change the placeholder text in the box and `incremental` to support incremental (as you type) search.
98+
* Configures the search box. Set `placeholder` to change the placeholder text in the box and `incremental` to support incremental (as you type) search.
8699
*/
87100
box?: EuiFieldSearchProps & {
88101
// Boolean values are not meaningful to this EuiSearchBox, but are allowed so that other
@@ -92,7 +105,7 @@ export interface EuiSearchBarProps extends CommonProps {
92105
};
93106

94107
/**
95-
An array of search filters. See {@link SearchFilterConfig}.
108+
* An array of search filters. See {@link SearchFilterConfig}.
96109
*/
97110
filters?: SearchFilterConfig[];
98111

0 commit comments

Comments
 (0)