Merge pull request #29401 from mario-mui/feat/search-plugin-i18n
search plugin support i18n
This commit is contained in:
@@ -14,3 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './blueprints';
|
||||
export { searchReactTranslationRef } from '../translation';
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
} from 'react';
|
||||
import useDebounce from 'react-use/esm/useDebounce';
|
||||
import { SearchContextProvider, useSearch } from '../../context';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { searchReactTranslationRef } from '../../translation';
|
||||
|
||||
/**
|
||||
* Props for {@link SearchBarBase}.
|
||||
@@ -81,6 +83,7 @@ export const SearchBarBase = forwardRef((props: SearchBarBaseProps, ref) => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const [value, setValue] = useState<string>('');
|
||||
const forwardedValueRef = useRef<string>('');
|
||||
const { t } = useTranslationRef(searchReactTranslationRef);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(prevValue => {
|
||||
@@ -129,12 +132,15 @@ export const SearchBarBase = forwardRef((props: SearchBarBaseProps, ref) => {
|
||||
}
|
||||
}, [onChange, onClear]);
|
||||
|
||||
const ariaLabel: string | undefined = label ? undefined : 'Search';
|
||||
const ariaLabel: string | undefined = label
|
||||
? undefined
|
||||
: t('searchBar.title');
|
||||
|
||||
const inputPlaceholder =
|
||||
placeholder ??
|
||||
`Search in ${configApi.getOptionalString('app.title') || 'Backstage'}`;
|
||||
|
||||
t('searchBar.placeholder', {
|
||||
org: configApi.getOptionalString('app.title') || 'Backstage',
|
||||
});
|
||||
const SearchIcon = useApp().getSystemIcon('search') || DefaultSearchIcon;
|
||||
|
||||
const startAdornment = (
|
||||
|
||||
@@ -14,14 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { screen, render, waitFor } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
import { SearchContextProvider } from '../../context';
|
||||
import { SearchFilter } from './SearchFilter';
|
||||
import { mockApis, TestApiProvider } from '@backstage/test-utils';
|
||||
import {
|
||||
mockApis,
|
||||
renderInTestApp,
|
||||
TestApiProvider,
|
||||
} from '@backstage/test-utils';
|
||||
import { searchApiRef } from '../../api';
|
||||
|
||||
describe('SearchFilter', () => {
|
||||
@@ -55,14 +59,16 @@ describe('SearchFilter', () => {
|
||||
it('Check that element was rendered and received props', async () => {
|
||||
const CustomFilter = (props: { name: string }) => <h6>{props.name}</h6>;
|
||||
|
||||
render(<SearchFilter name={name} component={CustomFilter} />);
|
||||
await renderInTestApp(
|
||||
<SearchFilter name={name} component={CustomFilter} />,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('heading', { name })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Checkbox', () => {
|
||||
it('Renders field name and values when provided as props', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -88,7 +94,7 @@ describe('SearchFilter', () => {
|
||||
});
|
||||
|
||||
it('Renders correctly based on filter state', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -119,7 +125,7 @@ describe('SearchFilter', () => {
|
||||
});
|
||||
|
||||
it('Renders correctly based on defaultValue', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -148,7 +154,7 @@ describe('SearchFilter', () => {
|
||||
});
|
||||
|
||||
it('Checking / unchecking a value sets filter state', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -185,7 +191,7 @@ describe('SearchFilter', () => {
|
||||
});
|
||||
|
||||
it('Checking / unchecking a value maintains unrelated filter state', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -226,7 +232,7 @@ describe('SearchFilter', () => {
|
||||
|
||||
describe('Select', () => {
|
||||
it('Renders field name and values when provided as props', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -258,7 +264,7 @@ describe('SearchFilter', () => {
|
||||
});
|
||||
|
||||
it('Renders values when provided asynchronously', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -299,7 +305,7 @@ describe('SearchFilter', () => {
|
||||
});
|
||||
|
||||
it('Renders correctly based on filter state', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -342,7 +348,7 @@ describe('SearchFilter', () => {
|
||||
});
|
||||
|
||||
it('Renders correctly based on defaultValue', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -383,7 +389,7 @@ describe('SearchFilter', () => {
|
||||
});
|
||||
|
||||
it('Selecting a value sets filter state', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -436,7 +442,7 @@ describe('SearchFilter', () => {
|
||||
});
|
||||
|
||||
it('Selecting a value maintains unrelated filter state', async () => {
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
} from './SearchFilter.Autocomplete';
|
||||
import { useAsyncFilterValues, useDefaultFilterValue } from './hooks';
|
||||
import { ensureFilterValueWithLabel, FilterValue } from './types';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { searchReactTranslationRef } from '../../translation';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
label: {
|
||||
@@ -165,6 +167,7 @@ export const SelectFilter = (props: SearchFilterComponentProps) => {
|
||||
values: givenValues,
|
||||
valuesDebounceMs,
|
||||
} = props;
|
||||
const { t } = useTranslationRef(searchReactTranslationRef);
|
||||
useDefaultFilterValue(name, defaultValue);
|
||||
const asyncValues =
|
||||
typeof givenValues === 'function' ? givenValues : undefined;
|
||||
@@ -179,7 +182,10 @@ export const SelectFilter = (props: SearchFilterComponentProps) => {
|
||||
valuesDebounceMs,
|
||||
);
|
||||
const allOptionValue = useRef(uuid());
|
||||
const allOption = { value: allOptionValue.current, label: 'All' };
|
||||
const allOption = {
|
||||
value: allOptionValue.current,
|
||||
label: t('searchFilter.allOptionTitle'),
|
||||
};
|
||||
const { filters, setFilters } = useSearch();
|
||||
|
||||
const handleChange = (value: SelectedItems) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import {
|
||||
mockApis,
|
||||
renderWithEffects,
|
||||
renderInTestApp,
|
||||
TestApiProvider,
|
||||
} from '@backstage/test-utils';
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('SearchPagination', () => {
|
||||
});
|
||||
|
||||
it('Renders without exploding', async () => {
|
||||
await renderWithEffects(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -74,7 +74,7 @@ describe('SearchPagination', () => {
|
||||
});
|
||||
|
||||
it('Define default page limit options', async () => {
|
||||
await renderWithEffects(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -99,7 +99,7 @@ describe('SearchPagination', () => {
|
||||
|
||||
it('Accept custom page limit label', async () => {
|
||||
const label = 'Page limit:';
|
||||
await renderWithEffects(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -116,7 +116,7 @@ describe('SearchPagination', () => {
|
||||
});
|
||||
|
||||
it('Show the total in text', async () => {
|
||||
await renderWithEffects(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -133,7 +133,7 @@ describe('SearchPagination', () => {
|
||||
});
|
||||
|
||||
it('Accept custom page limit text', async () => {
|
||||
await renderWithEffects(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -152,7 +152,7 @@ describe('SearchPagination', () => {
|
||||
});
|
||||
|
||||
it('Accept custom page limit options', async () => {
|
||||
await renderWithEffects(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -176,7 +176,7 @@ describe('SearchPagination', () => {
|
||||
});
|
||||
|
||||
it('Set page limit in the context', async () => {
|
||||
await renderWithEffects(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -208,7 +208,7 @@ describe('SearchPagination', () => {
|
||||
pageCursor: 'MQ==', // page: 1
|
||||
};
|
||||
|
||||
await renderWithEffects(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
@@ -250,7 +250,7 @@ describe('SearchPagination', () => {
|
||||
pageCursor: 'Mg==', // page: 2
|
||||
};
|
||||
|
||||
await renderWithEffects(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[searchApiRef, searchApiMock],
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
} from 'react';
|
||||
import TablePagination from '@material-ui/core/TablePagination';
|
||||
import { useSearch } from '../../context';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { searchReactTranslationRef } from '../../translation';
|
||||
|
||||
const encodePageCursor = (pageCursor: number): string => {
|
||||
return Buffer.from(pageCursor.toString(), 'utf-8').toString('base64');
|
||||
@@ -119,15 +121,18 @@ export type SearchPaginationBaseProps = {
|
||||
* @public
|
||||
*/
|
||||
export const SearchPaginationBase = (props: SearchPaginationBaseProps) => {
|
||||
const { t } = useTranslationRef(searchReactTranslationRef);
|
||||
const {
|
||||
total: count = -1,
|
||||
cursor: pageCursor,
|
||||
hasNextPage,
|
||||
onCursorChange: onPageCursorChange,
|
||||
limit: rowsPerPage = 25,
|
||||
limitLabel: labelRowsPerPage = 'Results per page:',
|
||||
limitLabel: labelRowsPerPage = t('searchPagination.limitLabel'),
|
||||
limitText: labelDisplayedRows = ({ from, to }) =>
|
||||
count > 0 ? `of ${count}` : `${from}-${to}`,
|
||||
count > 0
|
||||
? t('searchPagination.limitText', { num: `${count}` })
|
||||
: `${from}-${to}`,
|
||||
limitOptions: rowsPerPageOptions,
|
||||
onLimitChange: onPageLimitChange,
|
||||
...rest
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
SearchResultListItemExtensions,
|
||||
SearchResultListItemExtensionsProps,
|
||||
} from '../../extensions';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { searchReactTranslationRef } from '../../translation';
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultContext}
|
||||
@@ -186,11 +188,12 @@ export type SearchResultProps = Pick<SearchResultStateProps, 'query'> &
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultComponent = (props: SearchResultProps) => {
|
||||
const { t } = useTranslationRef(searchReactTranslationRef);
|
||||
const {
|
||||
query,
|
||||
children,
|
||||
noResultsComponent = (
|
||||
<EmptyState missing="data" title="Sorry, no results were found" />
|
||||
<EmptyState missing="data" title={t('noResultsDescription')} />
|
||||
),
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@@ -51,6 +51,8 @@ import { useSearchResultListItemExtensions } from '../../extensions';
|
||||
|
||||
import { DefaultResultListItem } from '../DefaultResultListItem';
|
||||
import { SearchResultState, SearchResultStateProps } from '../SearchResult';
|
||||
import { searchReactTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
listSubheader: {
|
||||
@@ -356,6 +358,7 @@ export function SearchResultGroupLayout<FilterOption>(
|
||||
) {
|
||||
const classes = useStyles();
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const { t } = useTranslationRef(searchReactTranslationRef);
|
||||
|
||||
const {
|
||||
error,
|
||||
@@ -365,7 +368,7 @@ export function SearchResultGroupLayout<FilterOption>(
|
||||
titleProps = {},
|
||||
link = (
|
||||
<>
|
||||
See all
|
||||
{t('searchResultGroup.linkTitle')}
|
||||
<ArrowRightIcon className={classes.listSubheaderLinkIcon} />
|
||||
</>
|
||||
),
|
||||
@@ -387,7 +390,7 @@ export function SearchResultGroupLayout<FilterOption>(
|
||||
),
|
||||
disableRenderingWithNoResults,
|
||||
noResultsComponent = disableRenderingWithNoResults ? null : (
|
||||
<EmptyState missing="data" title="Sorry, no results were found" />
|
||||
<EmptyState missing="data" title={t('noResultsDescription')} />
|
||||
),
|
||||
...rest
|
||||
} = props;
|
||||
@@ -434,7 +437,7 @@ export function SearchResultGroupLayout<FilterOption>(
|
||||
component="button"
|
||||
icon={<AddIcon />}
|
||||
variant="outlined"
|
||||
label="Add filter"
|
||||
label={t('searchResultGroup.addFilterButtonTitle')}
|
||||
aria-controls="filters-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -30,6 +30,8 @@ import { useSearchResultListItemExtensions } from '../../extensions';
|
||||
|
||||
import { DefaultResultListItem } from '../DefaultResultListItem';
|
||||
import { SearchResultState, SearchResultStateProps } from '../SearchResult';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { searchReactTranslationRef } from '../../translation';
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultListLayout}
|
||||
@@ -72,6 +74,7 @@ export type SearchResultListLayoutProps = ListProps & {
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultListLayout = (props: SearchResultListLayoutProps) => {
|
||||
const { t } = useTranslationRef(searchReactTranslationRef);
|
||||
const {
|
||||
error,
|
||||
loading,
|
||||
@@ -84,7 +87,7 @@ export const SearchResultListLayout = (props: SearchResultListLayoutProps) => {
|
||||
),
|
||||
disableRenderingWithNoResults,
|
||||
noResultsComponent = disableRenderingWithNoResults ? null : (
|
||||
<EmptyState missing="data" title="Sorry, no results were found" />
|
||||
<EmptyState missing="data" title={t('noResultsDescription')} />
|
||||
),
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@@ -20,6 +20,8 @@ import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
|
||||
import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
|
||||
|
||||
import { useSearch } from '../../context';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { searchReactTranslationRef } from '../../translation';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
@@ -36,6 +38,7 @@ const useStyles = makeStyles(theme => ({
|
||||
export const SearchResultPager = () => {
|
||||
const { fetchNextPage, fetchPreviousPage } = useSearch();
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(searchReactTranslationRef);
|
||||
|
||||
if (!fetchNextPage && !fetchPreviousPage) {
|
||||
return <></>;
|
||||
@@ -49,7 +52,7 @@ export const SearchResultPager = () => {
|
||||
onClick={fetchPreviousPage}
|
||||
startIcon={<ArrowBackIosIcon />}
|
||||
>
|
||||
Previous
|
||||
{t('searchResultPager.previous')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -58,7 +61,7 @@ export const SearchResultPager = () => {
|
||||
onClick={fetchNextPage}
|
||||
endIcon={<ArrowForwardIosIcon />}
|
||||
>
|
||||
Next
|
||||
{t('searchResultPager.next')}
|
||||
</Button>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const searchReactTranslationRef = createTranslationRef({
|
||||
id: 'search-react',
|
||||
messages: {
|
||||
searchBar: {
|
||||
title: 'Search',
|
||||
placeholder: 'Search in {{org}}',
|
||||
},
|
||||
searchFilter: {
|
||||
allOptionTitle: 'All',
|
||||
},
|
||||
searchPagination: {
|
||||
limitLabel: 'Results per page:',
|
||||
limitText: 'of {{num}}',
|
||||
},
|
||||
noResultsDescription: 'Sorry, no results were found',
|
||||
searchResultGroup: {
|
||||
linkTitle: 'See All',
|
||||
addFilterButtonTitle: 'Add filter',
|
||||
},
|
||||
searchResultPager: {
|
||||
previous: 'Previous',
|
||||
next: 'Next',
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user