ra-search
Plug your search engine and let users search across all resources via a smart Omnibox.
In large admins, users need several clicks to get to one record. For repetitive tasks, this ends up costing minutes every day. The ra-search
Omnibox simplifies navigation by providing a global, always-on search engine for records.
It even remembers the previous searches, so that end-users can easily find the record they were looking for.
Ra-search can take advantage of a search index like ElasticSearch if you have one, or it can rely on your REST API by searching across multiple resources in parallel.
Test it live in the Enterprise Edition Storybook and in the e-commerce demo.
Installation
npm install --save @react-admin/ra-search
# or
yarn add @react-admin/ra-search
Tip: ra-search
is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.
Data Provider For Global Search
ra-search
relies on the dataProvider
to communicate with the search engine. Whenever a user enters a search query, react-admin calls dataProvider.search(query)
. So for ra-search
to work, you must implement this method in your data provider.
Example Search Query
dataProvider.search('roll').then(response => console.log(response));
// {
// data: [
// { id: 'a7535', type: 'artist', url: '/artists/7535', content: { label: 'The Rolling Stones', description: 'English rock band formed in London in 1962' } }
// { id: 'a5352', type: 'artist', url: '/artists/5352', content: { label: 'Sonny Rollins', description: 'American jazz tenor saxophonist' } }
// { id: 't7524', type: 'track', url: '/tracks/7524', content: { label: 'Like a Rolling Stone', year: 1965, recordCompany: 'Columbia', artistId: 345, albumId: 435456 } }
// { id: 't2386', type: 'track', url: '/tracks/2386', content: { label: "It's Only Rock 'N Roll (But I Like It)", year: 1974, artistId: 7535, albumId: 6325 } }
// { id: 'a6325', type: 'album', url: '/albums/6325', content: { label: "It's Only rock 'N Roll", year: 1974, artistId: 7535 }}
// ],
// total: 5
// }
dataProvider.search("roll").then((response) => console.log(response));
// {
// data: [
// { id: 'a7535', type: 'artist', url: '/artists/7535', content: { label: 'The Rolling Stones', description: 'English rock band formed in London in 1962' } }
// { id: 'a5352', type: 'artist', url: '/artists/5352', content: { label: 'Sonny Rollins', description: 'American jazz tenor saxophonist' } }
// { id: 't7524', type: 'track', url: '/tracks/7524', content: { label: 'Like a Rolling Stone', year: 1965, recordCompany: 'Columbia', artistId: 345, albumId: 435456 } }
// { id: 't2386', type: 'track', url: '/tracks/2386', content: { label: "It's Only Rock 'N Roll (But I Like It)", year: 1974, artistId: 7535, albumId: 6325 } }
// { id: 'a6325', type: 'album', url: '/albums/6325', content: { label: "It's Only rock 'N Roll", year: 1974, artistId: 7535 }}
// ],
// total: 5
// }
Input and Output Formats
The dataProvider.search()
method should return a Promise for data containing an array of SearchResult
objects. A SearchResult
contains at least the following fields:
id
:Identifier
The unique identifier of the search resulttype
:string
An arbitrary string which enables groupingurl
:string
The URL where to redirect to on click. It could be a custom page and not a resource if you want tocontent
:any
Can contain any data that will be used to display the result. If used with the default<SearchResultItem>
component, it must contain at least anid
,label
, and adescription
.matches
:any
An optional object containing an extract of the data with matches. Can be anything that will be interpreted by a<SearchResultItem>
As for the total
, it can be greater than the number of returned results. This is useful e.g. to show that there are more results.
It is your responsibility to add this search
method to your dataProvider
so that react-admin can send queries to and read responses from the search engine.
TypeScript Types
type search = (
query: string,
options?: SearchOptions
) => Promise<{ data: SearchResult[]; total: number }>;
interface SearchOptions {
targets?: string[];
[key: string]: any;
}
interface SearchResult {
id: Identifier;
type: string;
url: string;
content: any;
matches?: any;
}
addSearchMethod
Helper
If you don't have a full-text search endpoint in your API, you can use the addSearchMethod()
helper function. It adds a search()
method to an existing dataProvider
; this method calls dataProvider.getList()
on several resources in parallel, and aggregates the result into a single response.
For example, to add a dataProvider.search()
method that searches across the artists
, albums
, and tracks
resources, you can do:
import simpleRestProvider from 'ra-data-simple-rest';
import { addSearchMethod } from '@react-admin/ra-search';
const dataProvider = simpleRestProvider('http://path.to.my.api/');
const dataProviderWithSearch = addSearchMethod(dataProvider, [
'artists',
'tracks',
'albums',
]);
Calling dataProvider.search('roll')
issues the following queries in parallel:
dataProvider.getList('artists', { filter: { q: "roll" }})
dataProvider.getList('tracks', { filter: { q: "roll" }})
dataProvider.getList('albumns', { filter: { q: "roll" }})
Then aggregate the results and return them in a single response.
We don't recommend using addSearchMethod()
in production, because if there are many resources, the API may receive too many concurrent requests, and the generated dataProvider.search()
method is as slow as the slowest dataProvider.getList()
call. Instead, you should expose a search API endpoint, e.g. by using a search engine, and implement your own dataProvider.search()
method to convert the results to the format expected by ra-search
.
The second argument to addSearchMethod
is the builder configuration. It can be either an array of resource names or a map of the resources specifying how to format their records for search results.
The (optional) third argument to addSearchMethod
is the redirect option. It allows to choose whether the search results will redirect to the Show or the Edit page. You can also change this on a per-resource basis (see below). By default, search results redirect to the Edit page.
When called with an array of resources, addSearchMethod
populates the search results content
based on the records returned by dataProvider.getList()
, with the following inference rules:
id
: Returns the recordid
label
: Returns the recordlabel
orname
ortitle
description
: Returns the recorddescription
orbody
Example with an array of resources:
const dataProviderWithSearch = addSearchMethod(dataProvider, [
'artists',
'albums',
]);
When called with a map, each key being a resource name, the value can have the following properties:
label
: Either the field name to use as the label or a function that will be called with a record and must return a string. Defaults to the inference described above.description
: Either the field name to use as the description or a function that will be called with a record and must return a string. Defaults to the inference described above.redirect
: Optional. Argument that defines if the redirection is done on a show or edit page. In case the redirection is also defined globally, the per-resource one takes precedence.
Examples with a map of resources:
const dataProviderWithSearch = addSearchMethod(dataProvider, {
artists: {
label: 'full_name',
description: record =>
`${record.born_at}-${record.died_at} ${record.biography}`,
redirect: 'show',
},
albums: {
// no label specified, fallback on inference
description: record =>
`${record.released_at.getFullYear()} by ${record.recordCompany}`,
},
});
const dataProviderWithSearchAndGlobalRedirection = addSearchMethod(
dataProvider,
{
artists: {
label: 'full_name',
description: record =>
`${record.born_at}-${record.died_at} ${record.biography}`,
},
albums: {
// no label specified, fallback on inference
description: record =>
`${record.released_at.getFullYear()} by ${
record.recordCompany
}`,
},
},
'show'
);
<Search>
The <Search>
component includes an input and displays the search results inside an MUI Popover
.
By default, it will group the search results by target, and show their content.label
and content.description
.
Usage
Include the <Search>
component inside a custom <AppBar>
component:
import { Admin, AppBar, TitlePortal, Layout } from 'react-admin';
import { Search } from '@react-admin/ra-search';
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<Search />
</AppBar>
);
const MyLayout = props => <Layout {...props} appBar={MyAppBar} />;
export const App = () => (
<Admin dataProvider={searchDataProvider} layout={MyLayout}>
// ...
</Admin>
);
import { Admin, AppBar, TitlePortal, Layout } from "react-admin";
import { Search } from "@react-admin/ra-search";
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<Search />
</AppBar>
);
const MyLayout = (props) => <Layout {...props} appBar={MyAppBar} />;
export const App = () => (
<Admin dataProvider={searchDataProvider} layout={MyLayout}>
// ...
</Admin>
);
Props
The <Search>
component accepts the following props:
Prop | Required | Type | Default | Description |
---|---|---|---|---|
children |
Optional | Element |
<SearchResultsPanel> |
A component that will display the results. |
disableHighlight |
Optional | boolean |
false |
Disable the highlight of the search term of each result. |
historySize |
Optional | number |
5 | The number of past queries to keep in history. |
isInAppBar |
Optional | boolean |
true |
Apply a dedicated style to the <AppBar> if true |
options |
Optional | Object |
- | An object containing options to apply to the search. |
queryOptions |
Optional | UseQuery Options |
- | react-query options for the search query |
wait |
Optional | number |
500 | The delay of debounce for the search to launch after typing in ms. |
Additional props are passed down to the Material UI <TextField>
component.
children
The <Search>
children allow you to customize the way results are displayed. The child component can grab the search result using the useSearchResult
hook.
import { Admin, AppBar, TitlePortal, Layout } from 'react-admin';
import { Search, useSearchResult } from '@react-admin/ra-search';
const CustomSearchResultsPanel = () => {
const { data, onClose } = useSearchResult();
return (
<ul>
{data.map(searchResult => (
<li key={searchResult.id}>{searchResult.content.label}</li>
))}
</ul>
);
};
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<Search>
<CustomSearchResultsPanel />
</Search>
</AppBar>
);
const MyLayout = props => <Layout {...props} appBar={MyAppBar} />;
export const App = () => (
<Admin dataProvider={searchDataProvider} layout={MyLayout}>
// ...
</Admin>
);
import { Admin, AppBar, TitlePortal, Layout } from "react-admin";
import { Search, useSearchResult } from "@react-admin/ra-search";
const CustomSearchResultsPanel = () => {
const { data, onClose } = useSearchResult();
return (
<ul>
{data.map((searchResult) => (
<li key={searchResult.id}>{searchResult.content.label}</li>
))}
</ul>
);
};
const MyAppBar = () => (
<AppBar>
<TitlePortal />
<Search>
<CustomSearchResultsPanel />
</Search>
</AppBar>
);
const MyLayout = (props) => <Layout {...props} appBar={MyAppBar} />;
export const App = () => (
<Admin dataProvider={searchDataProvider} layout={MyLayout}>
// ...
</Admin>
);
disableHighlight
The search terms in each result are highlighted. You can disable this feature with the disableHighlight
prop as follows:
<Search disableHighlight />
<Search disableHighlight />;
Tip: To customize the highlight style check out the Customizing the result items section below.
historySize
The number of previous user searches to keep in the popover. For example, if a user performs 10 searches and historySize
is set to 5, the popover will display the user's last 5 queries.
<Search historySize={5} />
<Search historySize={5} />;
isInAppBar
The <Search>
component has a specific style to fit the admin appBar. If you need to render the <Search>
component elsewhere, you can set isInAppBar
to false
and hence apply the default MUI style.
<Search isInAppBar={false} />
<Search isInAppBar={false} />;
options
An object containing options to apply to the search:
targets
:string[]
: an array of the indices on which to perform the search. Defaults to an empty array.{any}
:{any}
: any custom option to pass to the search engine.
<Search options={{ foo: 'bar' }} />
<Search options={{ foo: "bar" }} />;
queryOptions
<Search>
accepts a queryOptions
prop to pass options to the react-query client.
This can be useful e.g. to override the default side effects such as onSuccess
or onError
.
<Search queryOptions={{ onSuccess: data => console.log(data) }} />
<Search queryOptions={{ onSuccess: (data) => console.log(data) }} />;
wait
The number of milliseconds to wait before processing the search request, immediately after the user enters their last character.
<Search wait={200} />
<Search wait={200} />;
Customizing the Entire Search Results
Pass a custom React element as a child of <Search>
to customize the appearance of the search results. This can be useful e.g. to customize the results grouping, or to arrange search results differently.
ra-search
renders the <Search>
inside a SearchContext
. You can use the useSearchResult
hook to read the search results, as follows:
import { Search, useSearchResult } from '@react-admin/ra-search';
const MySearch = props => (
<Search>
<CustomSearchResultsPanel />
</Search>
);
const CustomSearchResultsPanel = () => {
const { data, onClose } = useSearchResult();
return (
<ul>
{data.map(searchResult => (
<li key={searchResult.id}>
<Link to={searchResult.url} onClick={onClose}>
<strong>{searchResult.content.label}</strong>
</Link>
<p>{searchResult.content.description}</p>
</li>
))}
</ul>
);
};
import { Search, useSearchResult } from "@react-admin/ra-search";
const MySearch = (props) => (
<Search>
<CustomSearchResultsPanel />
</Search>
);
const CustomSearchResultsPanel = () => {
const { data, onClose } = useSearchResult();
return (
<ul>
{data.map((searchResult) => (
<li key={searchResult.id}>
<Link to={searchResult.url} onClick={onClose}>
<strong>{searchResult.content.label}</strong>
</Link>
<p>{searchResult.content.description}</p>
</li>
))}
</ul>
);
};
Customizing The Result Items
By default, <Search>
displays the results in <SearchResultsPanel>
, which displays each results in a <SearchResultItem>
. So rendering <Search>
without children is equivalent to rendering:
const MySearch = () => (
<Search>
<SearchResultsPanel>
<SearchResultItem />
</SearchResultsPanel>
</Search>
);
const MySearch = () => (
<Search>
<SearchResultsPanel>
<SearchResultItem />
</SearchResultsPanel>
</Search>
);
<SearchResultItem>
renders the content.label
and content.description
for each result. You can customize what it renders by providing a function as the label
and the description
props. This function takes the search result as a parameter and must return a React element.
For instance:
import {
Search,
SearchResultsPanel,
SearchResultItem,
} from '@react-admin/ra-search';
const MySearch = () => (
<Search>
<SearchResultsPanel>
<SearchResultItem
label={record => (
<>
{record.type === 'artists' ? (
<PersonIcon />
) : (
<MusicIcon />
)}
<span>{record.content.label}</span>
</>
)}
/>
</SearchResultsPanel>
</Search>
);
import { Search, SearchResultsPanel, SearchResultItem } from "@react-admin/ra-search";
const MySearch = () => (
<Search>
<SearchResultsPanel>
<SearchResultItem
label={(record) => (
<>
{record.type === "artists" ? <PersonIcon /> : <MusicIcon />}
<span>{record.content.label}</span>
</>
)}
/>
</SearchResultsPanel>
</Search>
);
You can also completely replace the search result item component:
import { Search, SearchResultsPanel } from '@react-admin/ra-search';
const MySearchResultItem = ({ data, onClose }) => (
<li key={data.id} className="highlight">
<Link to={data.url} onClick={onClose}>
<strong>{data.content.label}</strong>
</Link>
<p>{data.content.description}</p>
</li>
);
const MySearch = () => (
<Search>
<SearchResultsPanel>
<MySearchResultItem />
</SearchResultsPanel>
</Search>
);
import { Search, SearchResultsPanel } from "@react-admin/ra-search";
const MySearchResultItem = ({ data, onClose }) => (
<li key={data.id} className="highlight">
<Link to={data.url} onClick={onClose}>
<strong>{data.content.label}</strong>
</Link>
<p>{data.content.description}</p>
</li>
);
const MySearch = () => (
<Search>
<SearchResultsPanel>
<MySearchResultItem />
</SearchResultsPanel>
</Search>
);
Tip: You can customize the highlight of the search terms by overriding the <SearchResultsPanel sx>
prop as following:
const CustomSearch = () => (
<Search>
<SearchResultsPanel
sx={{
'& ::highlight(search)': {
backgroundColor: '#7de5fa',
},
}}
/>
</Search>
);
<SearchWithResult>
The <SearchWithResult>
component renders a search input and the search results directly below the input. It's ideal for dashboards or menu panels.
By default, <SearchWithResult>
will group the search results by target, and show their content.label
and content.description
.
Usage
Here's how to include the <SearchWithResult>
component inside a custom <Dashboard>
component:
import { Card, CardContent } from '@mui/material';
import { Admin } from 'react-admin';
import { SearchWithResult } from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';
const MyDashboard = () => (
<Card>
<CardContent>
<SearchWithResult />
</CardContent>
</Card>
);
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
import { Card, CardContent } from "@mui/material";
import { Admin } from "react-admin";
import { SearchWithResult } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";
const MyDashboard = () => (
<Card>
<CardContent>
<SearchWithResult />
</CardContent>
</Card>
);
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
Props
Prop | Required | Type | Default | Description |
---|---|---|---|---|
children |
Optional | Element |
<SearchResultsPanel> |
A component that will display the results. |
disableHighlight |
Optional | boolean |
false |
Disable the highlight of the search term of each result. |
onNavigate |
Optional | function |
() => undefined |
A callback function to run when the user navigate to a result. |
options |
Optional | Object |
- | An object containing options to apply to the search. |
queryOptions |
Optional | UseQuery Options |
- | react-query options for the search query |
wait |
Optional | number |
500 | The delay of debounce for the search to launch after typing in ms. |
children
The <SearchWithResult>
children allow you to customize the way results are displayed. The child component can grab the search result using the useSearchResult
hook.
import { Admin } from 'react-admin';
import { SearchWithResult, useSearchResults } from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';
const MyDashboard = () => (
<SearchWithResult>
<MySearchResultsPanel />
</SearchWithResult>
);
const MySearchResultsPanel = () => {
const { data } = useSearchResults();
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.content.label}</li>
))}
</ul>
);
};
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
import { Admin } from "react-admin";
import { SearchWithResult, useSearchResults } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";
const MyDashboard = () => (
<SearchWithResult>
<MySearchResultsPanel />
</SearchWithResult>
);
const MySearchResultsPanel = () => {
const { data } = useSearchResults();
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.content.label}</li>
))}
</ul>
);
};
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
disableHighlight
The search terms in each result are highlighted. You can disable this feature with the disableHighlight
prop as follows:
<SearchWithResults disableHighlight />
<SearchWithResults disableHighlight />;
Tip: To customize the highlight style check out the Customizing the result items section below.
onNavigate
onNavigate
allows you to perform an action when the user clicks on a search result, e.g. to close a menu (See below for an example with <SolarLayout>
).
import { Admin } from 'react-admin';
import { SearchWithResult } from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';
const MyDashboard = () => {
const handleNavigate = () => {
console.log('User navigated to a result');
};
return <SearchWithResult onNavigate={handleNavigate} />;
};
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
import { Admin } from "react-admin";
import { SearchWithResult } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";
const MyDashboard = () => {
const handleNavigate = () => {
console.log("User navigated to a result");
};
return <SearchWithResult onNavigate={handleNavigate} />;
};
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
options
An object containing options to apply to the search:
targets
:string[]
: an array of the indices on which to perform the search. Defaults to an empty array.{any}
:{any}
: any custom option to pass to the search engine.
<SearchWithResult options={{ foo: 'bar' }} />
<SearchWithResult options={{ foo: "bar" }} />;
queryOptions
<SearchWithResult>
accepts a queryOptions
prop to pass options to the react-query client.
This can be useful e.g. to override the default side effects such as onSuccess
or onError
.
<SearchWithResult queryOptions={{ onSuccess: data => console.log(data) }} />
<SearchWithResult queryOptions={{ onSuccess: (data) => console.log(data) }} />;
wait
The number of milliseconds to wait before processing the search request, immediately after the user enters their last character.
<SearchWithResult wait={200} />
<SearchWithResult wait={200} />;
Customizing the Entire Search Results
Pass a custom React element as a child of <SearchWithResult>
to customize the appearance of the search results. This can be useful e.g. to customize the results grouping, or to arrange search results differently.
ra-search
renders the <SearchResultsPanel>
inside a SearchContext
. You can use the useSearchResult
hook to read the search results, as follows:
import { Card, CardContent } from '@mui/material';
import { Link } from 'react-router-dom';
import { Admin } from 'react-admin';
import {
SearchWithResult,
SearchResultsPanel,
useSearchResults,
} from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';
const MyDashboard = () => (
<Card>
<CardContent>
<SearchWithResult>
<MySearchResultsPanel />
</SearchWithResult>
</CardContent>
</Card>
);
const MySearchResultsPanel = () => {
const { data } = useSearchResults();
return (
<ul style={{ maxHeight: '250px', overflow: 'auto' }}>
{data.map(item => (
<li key={item.id}>
<Link to={item.url}>
<strong>{item.content.label}</strong>
</Link>
<p>{item.content.description}</p>
</li>
))}
</ul>
);
};
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
import { Card, CardContent } from "@mui/material";
import { Link } from "react-router-dom";
import { Admin } from "react-admin";
import { SearchWithResult, useSearchResults } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";
const MyDashboard = () => (
<Card>
<CardContent>
<SearchWithResult>
<MySearchResultsPanel />
</SearchWithResult>
</CardContent>
</Card>
);
const MySearchResultsPanel = () => {
const { data } = useSearchResults();
return (
<ul style={{ maxHeight: "250px", overflow: "auto" }}>
{data.map((item) => (
<li key={item.id}>
<Link to={item.url}>
<strong>{item.content.label}</strong>
</Link>
<p>{item.content.description}</p>
</li>
))}
</ul>
);
};
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
Customizing The Result Items
By default, <SearchWithResult>
displays the results in <SearchResultsPanel>
, which displays each results in a <SearchResultItem>
. So rendering <SearchWithResult>
without children is equivalent to rendering:
const MySearch = () => (
<SearchWithResult>
<SearchResultsPanel>
<SearchResultItem />
</SearchResultsPanel>
</SearchWithResult>
);
const MySearch = () => (
<SearchWithResult>
<SearchResultsPanel>
<SearchResultItem />
</SearchResultsPanel>
</SearchWithResult>
);
<SearchResultItem>
renders the content.label
and content.description
for each result. You can customize what it renders by providing a function as the label
and the description
props. This function takes the search result as a parameter and must return a React element.
For instance:
import { Card, CardContent } from '@mui/material';
import Groups3Icon from '@mui/icons-material/Groups3';
import LibraryMusicIcon from '@mui/icons-material/LibraryMusic';
import { Admin } from 'react-admin';
import {
SearchWithResult,
SearchResultsPanel,
SearchResultItem,
useSearchResults,
} from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';
const MyDashboard = () => (
<Card>
<CardContent>
<SearchWithResult>
<SearchResultsPanel>
<SearchResultItem
label={record => (
<>
{record.type === 'artists' ? (
<Groups3Icon />
) : (
<LibraryMusicIcon />
)}
<span>{record.content.label}</span>
</>
)}
/>
</SearchResultsPanel>
</SearchWithResult>
</CardContent>
</Card>
);
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
import { Card, CardContent } from "@mui/material";
import Groups3Icon from "@mui/icons-material/Groups3";
import LibraryMusicIcon from "@mui/icons-material/LibraryMusic";
import { Admin } from "react-admin";
import { SearchWithResult, SearchResultsPanel, SearchResultItem } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";
const MyDashboard = () => (
<Card>
<CardContent>
<SearchWithResult>
<SearchResultsPanel>
<SearchResultItem
label={(record) => (
<>
{record.type === "artists" ? <Groups3Icon /> : <LibraryMusicIcon />}
<span>{record.content.label}</span>
</>
)}
/>
</SearchResultsPanel>
</SearchWithResult>
</CardContent>
</Card>
);
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
You can also completely replace the search result item component:
import { Card, CardContent } from '@mui/material';
import { Link } from 'react-router-dom';
import { Admin } from 'react-admin';
import {
SearchWithResult,
SearchResultsPanel,
SearchResultItem,
} from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';
const MySearchResultItem = ({ data }) => (
<li key={data.id} className="highlight">
<Link to={data.url}>
<strong>{data.content.label}</strong>
</Link>
<p>{data.content.description}</p>
</li>
);
const MyDashboard = () => (
<Card>
<CardContent>
<SearchWithResult>
<SearchResultsPanel>
<MySearchResultItem />
</SearchResultsPanel>
</SearchWithResult>
</CardContent>
</Card>
);
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
import { Card, CardContent } from "@mui/material";
import { Link } from "react-router-dom";
import { Admin } from "react-admin";
import { SearchWithResult, SearchResultsPanel } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";
const MySearchResultItem = ({ data }) => (
<li key={data.id} className="highlight">
<Link to={data.url}>
<strong>{data.content.label}</strong>
</Link>
<p>{data.content.description}</p>
</li>
);
const MyDashboard = () => (
<Card>
<CardContent>
<SearchWithResult>
<SearchResultsPanel>
<MySearchResultItem />
</SearchResultsPanel>
</SearchWithResult>
</CardContent>
</Card>
);
export const App = () => (
<Admin dataProvider={searchDataProvider} dashboard={MyDashboard}>
{/*...*/}
</Admin>
);
Tip: You can customize the highlight of the search terms by overriding the <SearchResultsPanel sx>
prop as following:
const CustomSearch = () => (
<SearchWithResult>
<SearchResultsPanel
sx={{
'& ::highlight(search)': {
backgroundColor: '#7de5fa',
},
}}
/>
</SearchWithResult>
);
Use It With SolarLayout
The <SearchWithResult>
component works perfectly when used inside the <SolarLayout>
menu.
The useSolarSidebarActiveMenu
hook combined with the onNavigate
prop allow you to close the <SolarMenu>
when the user selects an element in the result.
Here is an implementation example:
import { Admin } from 'react-admin';
import { Box } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AlbumIcon from '@mui/icons-material/Album';
import Groups3Icon from '@mui/icons-material/Groups3';
import {
SolarLayout,
SolarLayoutProps,
SolarMenu,
useSolarSidebarActiveMenu,
} from '@react-admin/ra-navigation';
import { SearchWithResult } from '@react-admin/ra-search';
import { searchDataProvider } from './searchDataProvider';
const MySolarLayout = (props: SolarLayoutProps) => (
<SolarLayout {...props} menu={MySolarMenu} />
);
const MySolarMenu = () => (
<SolarMenu bottomToolbar={<CustomBottomToolbar />}>
<SolarMenu.Item
name="artists"
to="/artists"
icon={<Groups3Icon />}
label="resources.stores.name"
/>
<SolarMenu.Item
name="songs"
to="/songs"
icon={<AlbumIcon />}
label="resources.events.name"
/>
</SolarMenu>
);
const CustomBottomToolbar = () => (
<>
<SearchMenuItem />
<SolarMenu.LoadingIndicatorItem />
</>
);
const SearchMenuItem = () => {
const [, setActiveMenu] = useSolarSidebarActiveMenu();
const handleClose = () => {
setActiveMenu('');
};
return (
<SolarMenu.Item
icon={<SearchIcon />}
label="Search"
name="search"
subMenu={
<Box sx={{ maxWidth: 298 }}>
<SearchWithResult onNavigate={handleClose} />
</Box>
}
data-testid="search-button"
/>
);
};
export const App = () => (
<Admin dataProvider={searchDataProvider} layout={MySolarLayout}>
{/*...*/}
</Admin>
);
import { Admin } from "react-admin";
import { Box } from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import AlbumIcon from "@mui/icons-material/Album";
import Groups3Icon from "@mui/icons-material/Groups3";
import { SolarLayout, SolarMenu, useSolarSidebarActiveMenu } from "@react-admin/ra-navigation";
import { SearchWithResult } from "@react-admin/ra-search";
import { searchDataProvider } from "./searchDataProvider";
const MySolarLayout = (props) => <SolarLayout {...props} menu={MySolarMenu} />;
const MySolarMenu = () => (
<SolarMenu bottomToolbar={<CustomBottomToolbar />}>
<SolarMenu.Item name="artists" to="/artists" icon={<Groups3Icon />} label="resources.stores.name" />
<SolarMenu.Item name="songs" to="/songs" icon={<AlbumIcon />} label="resources.events.name" />
</SolarMenu>
);
const CustomBottomToolbar = () => (
<>
<SearchMenuItem />
<SolarMenu.LoadingIndicatorItem />
</>
);
const SearchMenuItem = () => {
const [, setActiveMenu] = useSolarSidebarActiveMenu();
const handleClose = () => {
setActiveMenu("");
};
return (
<SolarMenu.Item
icon={<SearchIcon />}
label="Search"
name="search"
subMenu={
<Box sx={{ maxWidth: 298 }}>
<SearchWithResult onNavigate={handleClose} />
</Box>
}
data-testid="search-button"
/>
);
};
export const App = () => (
<Admin dataProvider={searchDataProvider} layout={MySolarLayout}>
{/*...*/}
</Admin>
);
useSearch
Just like useUpdate
, useSearch
returns a function allowing to call the dataProvider.search()
, as well as a state object for the response. Use it to create your own <Search>
component.
import { useState } from 'React';
import { TextField } from '@mui/material';
import {
useSearch,
SearchResultContextProvider,
SearchResultsPanel,
} from '@react-admin/ra-search';
const Search = () => {
const [query, setQuery] = useState<string | undefined>();
const [open, setOpen] = useState(false);
const [search, searchResultState] = useSearch();
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
setQuery(event.target.value);
search(event.target.value);
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const contextValue = useMemo(() => ({
onClose: handleClose,
...searchResultState,
}));
return (
<>
<TextField value={query} onChange={handleChange} />
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
>
<Typography id="modal-modal-title" variant="h6" component="h2">
Search results for: {searchResultState.query}
</Typography>
<SearchResultContextProvider value={contextValue}>
<SearchResultsPanel />
</SearchResultContextProvider>
</Modal>
</>
);
};
import { useState } from "React";
import { TextField } from "@mui/material";
import { useSearch, SearchResultContextProvider, SearchResultsPanel } from "@react-admin/ra-search";
const Search = () => {
const [query, setQuery] = useState();
const [open, setOpen] = useState(false);
const [search, searchResultState] = useSearch();
const handleChange = (event) => {
setQuery(event.target.value);
search(event.target.value);
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const contextValue = useMemo(() => ({
onClose: handleClose,
...searchResultState,
}));
return (
<>
<TextField value={query} onChange={handleChange} />
<Modal open={open} onClose={handleClose} aria-labelledby="modal-modal-title">
<Typography id="modal-modal-title" variant="h6" component="h2">
Search results for: {searchResultState.query}
</Typography>
<SearchResultContextProvider value={contextValue}>
<SearchResultsPanel />
</SearchResultContextProvider>
</Modal>
</>
);
};
Navigating In Search Results With Arrow Keys
By default, the <Search>
component allows the user to navigate search results with the arrow keys. If you want to reimplement it, you have to use the useArrowKeysToNavigate
hook.
Pass the list ref to the hook; also, each result must have a button role:
import { List, ListItem } from '@mui/material';
const SearchResults = () => {
const listRef = React.useRef<HTMLUListElement>(null);
useArrowKeysToNavigate(listRef);
<List ref={listRef}>
{data.map(resultData => {
return <ListItem button data={resultData} key={resultData.id} />;
})}
</List>;
};
import { List, ListItem } from "@mui/material";
const SearchResults = () => {
const listRef = React.useRef(null);
useArrowKeysToNavigate(listRef);
<List ref={listRef}>
{data.map((resultData) => {
return <ListItem button data={resultData} key={resultData.id} />;
})}
</List>;
};
I18N
You can customize the <Search>
component text, by importing the custom messages into your i18nProvider
:
// in src/i18nProvider.ts
import { mergeTranslations } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import {
raSearchEnglishMessages,
raSearchFrenchMessages,
} from '@react-admin/ra-search';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
export const i18nProvider = polyglotI18nProvider(locale => {
if (locale === 'fr') {
return mergeTranslations(frenchMessages, raSearchFrenchMessages);
}
// Always fallback on english
return mergeTranslations(englishMessages, raSearchEnglishMessages);
}, 'en');
// in src/i18nProvider.ts
import { mergeTranslations } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import { raSearchEnglishMessages, raSearchFrenchMessages } from "@react-admin/ra-search";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";
export const i18nProvider = polyglotI18nProvider((locale) => {
if (locale === "fr") {
return mergeTranslations(frenchMessages, raSearchFrenchMessages);
}
// Always fallback on english
return mergeTranslations(englishMessages, raSearchEnglishMessages);
}, "en");
CHANGELOG
v5.1.2
2024-08-19
- (fix) Bump
highlight-search-term
to 1.0.3 to fix the "Unexpected Token Export" error obtained when using the package as CJS
v5.1.1
2024-08-09
- (fix) Fix dependency to highlight-search-term.
v5.1.0
2024-08-06
- (feat) Highlight search term in
<Search>
and<SearchWithResults>
results
v5.0.0
2024-07-25
- Upgrade to react-admin v5
- The state returned by the
useSearch()
hook additionally returns alluseQuery
state - Fix
inflection
imports
Breaking Change
- The
useSearch()
hook takes an object as argument instead of a two arguments:
- const [search, searchData] = useSearch(
- {
- targets: ['artists', 'songs'],
- },
- {
- meta: { foo: 'bar' },
- }
- );
+ const [search, searchData] = useSearch({
+ options: {
+ targets: ['artists', 'songs'],
+ },
+ queryOptions: {
+ meta: { foo: 'bar' },
+ },
+ });
- The
QueryOptions
interface of theuseSearch()
hook was renammed toUseSearchOptions
- Remove
PropTypes
- the
data
prop returned by theuseSearchResults
hook might beundefined
. To avoid TypeScript complaints, you should check ifdata
isundefined
before using it:
const MySearchResult = (): ReactElement => {
const { data, onClose } = useSearchResults();
return (
<ul>
- {data.map(item => (
+ {data?.map(item => (
<li key={item.id}>
<Link to={item.url} onClick={onClose}>
{item.content.label}
</Link>
<p>{item.content.description}</p>
</li>
))}
</ul>
);
};
## v4.6.0
> 2024-04-05
- (feat) Add queryOptions prop to `<Search>` and `<SearchWithResult>`
## v4.5.3
> 2024-02-29
- (fix) Fix minimum required version of `@mui/material` and `@mui/icons-material`
## v4.5.2
> 2024-02-08
- (fix) Fix `<Search>` ignores the `historySize` and `options` props
- (fix) Fix `<SearchWithResult>` ignores the `options` prop
## v4.5.1
> 2023-12-01
- (fix) Fix `<Search>` prop types don't allow to customize `<TextField>` props
## v4.5.0
> 2023-11-10
- (fix) `<SearchInput>`, `<Search>` and `<SearchWithResult>` don't use MUI theme effectively.
- (feat) Introduce a new prop `isInAppBar` to `<Search>` component in order to use specific style for admin `appBar`.
**Breaking Change**
- `color` prop support is removed to delegate color and background color of components to MUI theme.
## v4.4.0
> 2023-10-15
- (feat) Add `<SearchWithResult>`, a search component for dashboards or menu panel, where the results appear below the search input rather than in a popover.
- (feat) Display total number of results per group in `<SearchResultsGroup>`'s subheader
## v4.3.0
> 2023-05-24
- Upgraded to react-admin `4.10.6`
## v4.2.0
> 2022-12-22
- (feat) Move search history from `useSearch` hook to `<Search>` component
- (fix) Fix history shows too many items
- (fix) Fix UI glitches in `<SearchInput>`
- (fix) Fix `<Search>` component requires an i18n provider to be set
## v4.1.2
> 2022-11-30
- (feat) Use `react-query` to manage `useSearch` hook queries
## v4.1.1
> 2022-10-20
- (feat) Export `SearchHistoryPanel` and `SearchHistoryItem` components.
## v4.1.0
> 2022-08-19
- (feat) Add `redirect` option to `addSearchMethod`, offering ability to choose the page to redirect to (edit or show)
## v4.0.5
> 2022-08-18
- (doc) Fix documentation still mentioning `useSearchResultContext` instead of `useSearchResults`
## v4.0.4
> 2022-08-09
- (fix) Backport: Fix `onSelect` parameter types
## v4.0.3
> 2022-06-28
- (doc) Fix documentation referencing non existent hooks
- (doc) Fix documentation incorrect instructions to setup of the `SearchContext`
## v4.0.2
> 2022-06-14
- (fix) Use theme `shape.borderRadius` settings for hover effect
## v4.0.1
> 2022-06-08
- (fix) Update peer dependencies ranges (support React 18)
## v4.0.0
> 2022-06-07
- Upgrade to react-admin v4
**Breaking Change**
- The state returned by `useSearch()` now uses `isLoading` instead of `loading`
```diff
-const [doSearch, { loading }] = useSearch('foo');
+const [doSearch, { isLoading }] = useSearch('foo');
v2.2.1
2021-06-29
- (fix) Update peer dependencies ranges (support react 17)
v2.2.0
2021-04-29
- (feat) Add a search history
v2.1.0
2021-04-22
- (feat) Allows users to navigate in results with up and down arrow keys
v2.0.1
2020-12-18
- (fix) Fix search results panel markup and accessibility
v2.0.0
2020-12-17
- (feat) rename search method options (facets -> targets)
v1.2.3
2020-12-08
- (fix) Fix SearchInput value is not selectable while search results are displayed
v1.2.2
2020-12-03
- (fix) Fix part of terms are sometimes discarded when response is slow
- (fix) Fix empty popover is displayed when there are no more results
v1.2.1
2020-11-23
- (fix) Fix search results popover closes and reopens at each keystroke
v1.2.0
2020-11-04
- (feat) Export groupSearchResultsByResource function
v1.1.1
2020-10-19
- (fix) Display the
<PopOver>
position at the bottom right of the<SearchInput>
- (fix) Remove the stickyness of facet headers
- (feat) Add a growing effect when focusing the menu
v1.1.0
2020-10-14
- (fix) Hide clear button when there is no value in the
<SearchInput>
- (feat) Add
color
prop to both<SearchInput>
and<Search>
component supportinglight
anddark
v1.0.0
2020-10-13
- First release