Merge pull request #29401 from mario-mui/feat/search-plugin-i18n

search plugin support i18n
This commit is contained in:
Alex Lorenzi
2025-05-07 12:42:34 -04:00
committed by GitHub
24 changed files with 263 additions and 68 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-search-react': patch
'@backstage/plugin-search': patch
---
search plugin support i18n
+18
View File
@@ -8,6 +8,7 @@ import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
import { ListItemProps } from '@material-ui/core/ListItem';
import { SearchDocument } from '@backstage/plugin-search-common';
import { SearchResult } from '@backstage/plugin-search-common';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
// @alpha (undocumented)
export type BaseSearchResultListItemProps<T = {}> = T & {
@@ -94,6 +95,23 @@ export interface SearchFilterResultTypeBlueprintParams {
value: string;
}
// @alpha (undocumented)
export const searchReactTranslationRef: TranslationRef<
'search-react',
{
readonly 'searchBar.title': 'Search';
readonly 'searchBar.placeholder': 'Search in {{org}}';
readonly 'searchFilter.allOptionTitle': 'All';
readonly 'searchPagination.limitLabel': 'Results per page:';
readonly 'searchPagination.limitText': 'of {{num}}';
readonly noResultsDescription: 'Sorry, no results were found';
readonly 'searchResultGroup.linkTitle': 'See All';
readonly 'searchResultGroup.addFilterButtonTitle': 'Add filter';
readonly 'searchResultPager.next': 'Next';
readonly 'searchResultPager.previous': 'Previous';
}
>;
// @alpha (undocumented)
export type SearchResultItemExtensionComponent = <
P extends BaseSearchResultListItemProps,
+1
View File
@@ -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>
);
+46
View File
@@ -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',
},
},
});
+15
View File
@@ -15,6 +15,7 @@ import { RouteRef } from '@backstage/frontend-plugin-api';
import { SearchFilterExtensionComponent } from '@backstage/plugin-search-react/alpha';
import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha';
import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
// @alpha (undocumented)
const _default: FrontendPlugin<
@@ -247,5 +248,19 @@ export const searchPage: ExtensionDefinition<{
};
}>;
// @alpha (undocumented)
export const searchTranslationRef: TranslationRef<
'search',
{
readonly 'searchModal.viewFullResults': 'View Full Results';
readonly 'searchType.tabs.allTitle': 'All';
readonly 'searchType.allResults': 'All Results';
readonly 'searchType.accordion.collapse': 'Collapse';
readonly 'searchType.accordion.allTitle': 'All';
readonly 'searchType.accordion.numberOfResults': '{{number}} results';
readonly 'sidebarSearchModal.title': 'Search';
}
>;
// (No @packageDocumentation comment for this package)
```
+3
View File
@@ -284,3 +284,6 @@ export default createFrontendPlugin({
root: rootRouteRef,
}),
});
/** @alpha */
export { searchTranslationRef } from './translation';
@@ -39,6 +39,8 @@ import { useNavigate } from 'react-router-dom';
import { rootRouteRef } from '../../plugin';
import { SearchResultSet } from '@backstage/plugin-search-common';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchTranslationRef } from '../../translation';
/**
* @public
@@ -121,6 +123,7 @@ export const Modal = ({
const navigate = useNavigate();
const { transitions } = useTheme();
const { focusContent } = useContent();
const { t } = useTranslationRef(searchTranslationRef);
const searchRootRoute = useRouteRef(rootRouteRef)();
const searchBarRef = useRef<HTMLInputElement | null>(null);
@@ -171,7 +174,7 @@ export const Modal = ({
onClick={handleSearchBarSubmit}
disableRipple
>
View Full Results
{t('searchModal.viewFullResults')}
</Button>
</Grid>
</Grid>
@@ -15,8 +15,12 @@
*/
import { ReactNode } from 'react';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { act, render, waitFor } from '@testing-library/react';
import {
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
import { act, waitFor } from '@testing-library/react';
import user from '@testing-library/user-event';
import {
searchApiRef,
@@ -81,7 +85,7 @@ describe('SearchType.Accordion', () => {
};
it('should render as expected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Accordion name={expectedLabel} types={[expectedType]} />
</Wrapper>,
@@ -103,7 +107,7 @@ describe('SearchType.Accordion', () => {
});
it('should set entire types array when a type is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Accordion name={expectedLabel} types={[expectedType]} />
</Wrapper>,
@@ -115,7 +119,7 @@ describe('SearchType.Accordion', () => {
});
it('should reset types array when all is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Accordion
name={expectedLabel}
@@ -131,7 +135,7 @@ describe('SearchType.Accordion', () => {
});
it('should reset page cursor when a new type is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Accordion name={expectedLabel} types={[expectedType]} />
</Wrapper>,
@@ -143,7 +147,7 @@ describe('SearchType.Accordion', () => {
});
it('should show result counts if enabled', async () => {
const { getAllByText } = render(
const { getAllByText } = await renderInTestApp(
<Wrapper>
<SearchType.Accordion
name={expectedLabel}
@@ -31,6 +31,8 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import Typography from '@material-ui/core/Typography';
import AllIcon from '@material-ui/icons/FontDownload';
import useAsync from 'react-use/esm/useAsync';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
icon: {
@@ -83,6 +85,7 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
const searchApi = useApi(searchApiRef);
const [expanded, setExpanded] = useState(true);
const { defaultValue, name, showCounts, types: givenTypes } = props;
const { t } = useTranslationRef(searchTranslationRef);
const toggleExpanded = () => setExpanded(prevState => !prevState);
const handleClick = (type: string) => {
@@ -103,7 +106,7 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
const definedTypes = [
{
value: '',
name: 'All',
name: t('searchType.accordion.allTitle'),
icon: <AllIcon />,
},
...givenTypes,
@@ -117,7 +120,7 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
const counts = await Promise.all(
definedTypes
.map(t => t.value)
.map(type => type.value)
.map(async type => {
const { numberOfResults } = await searchApi.query({
term,
@@ -130,9 +133,10 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
return [
type,
numberOfResults !== undefined
? `${
numberOfResults >= 10000 ? `>10000` : numberOfResults
} results`
? t('searchType.accordion.numberOfResults', {
number:
numberOfResults >= 10000 ? `>10000` : `${numberOfResults}`,
})
: ' -- ',
];
}),
@@ -160,8 +164,8 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
IconButtonProps={{ size: 'small' }}
>
{expanded
? 'Collapse'
: definedTypes.filter(t => t.value === selected)[0]!.name}
? t('searchType.accordion.collapse')
: definedTypes.filter(type => type.value === selected)[0]!.name}
</AccordionSummary>
<AccordionDetails classes={{ root: classes.accordionDetails }}>
<List
@@ -15,8 +15,12 @@
*/
import { ReactNode } from 'react';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { act, render } from '@testing-library/react';
import {
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
import { act } from '@testing-library/react';
import user from '@testing-library/user-event';
import {
SearchContextProvider,
@@ -69,7 +73,7 @@ describe('SearchType.Tabs', () => {
};
it('should render as expected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Tabs types={[expectedType]} />
</Wrapper>,
@@ -85,7 +89,7 @@ describe('SearchType.Tabs', () => {
});
it('should set entire types array when a type is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Tabs types={[expectedType]} />
</Wrapper>,
@@ -97,7 +101,7 @@ describe('SearchType.Tabs', () => {
});
it('should reset types array when all is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Tabs
defaultValue={expectedType.value}
@@ -112,7 +116,7 @@ describe('SearchType.Tabs', () => {
});
it('should reset page cursor when a new type is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Tabs types={[expectedType]} />
</Wrapper>,
@@ -20,6 +20,8 @@ import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import { makeStyles } from '@material-ui/core/styles';
import { Theme } from '@material-ui/core/styles';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchTranslationRef } from '../../translation';
const useStyles = makeStyles((theme: Theme) => ({
tabs: {
@@ -49,6 +51,7 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => {
const classes = useStyles();
const { setPageCursor, setTypes, types } = useSearch();
const { defaultValue, types: givenTypes } = props;
const { t } = useTranslationRef(searchTranslationRef);
const changeTab = (_: ChangeEvent<{}>, newType: string) => {
setTypes(newType !== '' ? [newType] : []);
@@ -66,7 +69,7 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => {
const definedTypes = [
{
value: '',
name: 'All',
name: t('searchType.tabs.allTitle'),
},
...givenTypes,
];
@@ -15,14 +15,18 @@
*/
import { configApiRef } from '@backstage/core-plugin-api';
import { render, screen, waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
SearchContextProvider,
searchApiRef,
} from '@backstage/plugin-search-react';
import { SearchType } from './SearchType';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import {
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
describe('SearchType', () => {
const initialState = {
@@ -53,7 +57,7 @@ describe('SearchType', () => {
describe('Type Filter', () => {
it('Renders field name and values when provided as props', async () => {
render(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
@@ -85,7 +89,7 @@ describe('SearchType', () => {
});
it('Renders correctly based on type filter state', async () => {
render(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
@@ -123,7 +127,7 @@ describe('SearchType', () => {
});
it('Renders correctly based on type filter defaultValue', async () => {
render(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
@@ -156,7 +160,7 @@ describe('SearchType', () => {
});
it('Selecting a value sets type filter state', async () => {
render(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
@@ -199,7 +203,7 @@ describe('SearchType', () => {
});
it('Selecting none defaults to empty state', async () => {
render(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
@@ -29,6 +29,8 @@ import {
} from './SearchType.Accordion';
import { SearchTypeTabs, SearchTypeTabsProps } from './SearchType.Tabs';
import { useSearch } from '@backstage/plugin-search-react';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
label: {
@@ -63,6 +65,7 @@ const SearchType = (props: SearchTypeProps) => {
const { className, defaultValue, name, values = [] } = props;
const classes = useStyles();
const { types, setTypes } = useSearch();
const { t } = useTranslationRef(searchTranslationRef);
useEffectOnce(() => {
if (!types.length) {
@@ -94,7 +97,7 @@ const SearchType = (props: SearchTypeProps) => {
variant="outlined"
value={types}
onChange={handleChange}
placeholder="All Results"
placeholder={t('searchType.allResults')}
renderValue={selected => (
<div className={classes.chips}>
{(selected as string[]).map(value => (
@@ -22,6 +22,8 @@ import {
SearchModalProvider,
useSearchModal,
} from '../SearchModal';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchTranslationRef } from '../../translation';
/**
* Props for {@link SidebarSearchModal}.
@@ -38,13 +40,14 @@ export type SidebarSearchModalProps = Pick<
const SidebarSearchModalContent = (props: SidebarSearchModalProps) => {
const { state, toggleModal } = useSearchModal();
const Icon = props.icon ? props.icon : SearchIcon;
const { t } = useTranslationRef(searchTranslationRef);
return (
<>
<SidebarItem
className="search-icon"
icon={Icon}
text="Search"
text={t('sidebarSearchModal.title')}
onClick={toggleModal}
/>
<SearchModal
+43
View File
@@ -0,0 +1,43 @@
/*
* 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 searchTranslationRef = createTranslationRef({
id: 'search',
messages: {
searchModal: {
viewFullResults: 'View Full Results',
},
searchType: {
allResults: 'All Results',
tabs: {
allTitle: 'All',
},
accordion: {
allTitle: 'All',
collapse: 'Collapse',
numberOfResults: '{{number}} results',
},
},
sidebarSearchModal: {
title: 'Search',
},
},
});