Merge pull request #29401 from mario-mui/feat/search-plugin-i18n
search plugin support i18n
This commit is contained in:
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user