diff --git a/plugins/search-react/alpha-api-report.md b/plugins/search-react/alpha-api-report.md index 90492f9434..163247bf5e 100644 --- a/plugins/search-react/alpha-api-report.md +++ b/plugins/search-react/alpha-api-report.md @@ -8,20 +8,22 @@ import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { Extension } from '@backstage/frontend-plugin-api'; import { ListItemProps } from '@material-ui/core'; +import { PortableSchema } from '@backstage/frontend-plugin-api'; import { SearchDocument } from '@backstage/plugin-search-common'; import { SearchResult } from '@backstage/plugin-search-common'; // @alpha (undocumented) -export type BaseSearchResultListItemProps = { +export type BaseSearchResultListItemProps = T & { rank?: number; result?: SearchDocument; - noTrack?: boolean; } & Omit; // @alpha (undocumented) -export const createSearchResultListItemExtension: ( - options: SearchResultItemExtensionOptions, -) => Extension; +export function createSearchResultListItemExtension< + TConfig extends { + noTrack?: boolean; + }, +>(options: SearchResultItemExtensionOptions): Extension; // @alpha (undocumented) export type SearchResultItemExtensionComponent = < @@ -40,10 +42,17 @@ export const searchResultItemExtensionData: ConfigurableExtensionDataRef< >; // @alpha (undocumented) -export type SearchResultItemExtensionOptions = { +export type SearchResultItemExtensionOptions< + TConfig extends { + noTrack?: boolean; + }, +> = { id: string; at: string; - component: () => Promise; + configSchema?: PortableSchema; + component: (options: { + config: TConfig; + }) => Promise; predicate?: SearchResultItemExtensionPredicate; }; diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 21dd6135a9..97c8300c14 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -391,11 +391,6 @@ export const SearchResultList: ( props: SearchResultListProps, ) => React_2.JSX.Element; -// @public -export const SearchResultListItemExtension: ( - props: SearchResultListItemExtensionProps, -) => React_2.JSX.Element; - // @public export type SearchResultListItemExtensionOptions< Component extends (props: any) => JSX.Element | null, diff --git a/plugins/search-react/src/extensions.tsx b/plugins/search-react/src/extensions.tsx index 5cd0aab76e..132fad25b5 100644 --- a/plugins/search-react/src/extensions.tsx +++ b/plugins/search-react/src/extensions.tsx @@ -86,7 +86,7 @@ export type SearchResultListItemExtensionProps = Props & >; /** - * @public + * @internal * Extends children with extension capabilities. * @param props - see {@link SearchResultListItemExtensionProps}. */ diff --git a/plugins/search-react/src/wiring/createSearchResultListItemExtension.test.tsx b/plugins/search-react/src/wiring/createSearchResultListItemExtension.test.tsx index d7a2947e1f..ca0e7e0672 100644 --- a/plugins/search-react/src/wiring/createSearchResultListItemExtension.test.tsx +++ b/plugins/search-react/src/wiring/createSearchResultListItemExtension.test.tsx @@ -22,6 +22,7 @@ import { createExtensionInput, createPageExtension, createPlugin, + createSchemaFromZod, } from '@backstage/frontend-plugin-api'; import { SearchResult } from '@backstage/plugin-search-common'; import { createApp } from '@backstage/frontend-app-api'; @@ -43,16 +44,32 @@ jest.mock('@backstage/plugin-graphiql', () => ({ describe('createSearchResultListItemExtension', () => { it('Should use the correct result component', async () => { + type TechDocsSearchReasulListItemProps = BaseSearchResultListItemProps<{ + lineClamp: number; + }>; const TechDocsSearchResultItemComponent = ( - props: BaseSearchResultListItemProps, - ) =>
TechDocs - Rank: {props.rank}
; + props: TechDocsSearchReasulListItemProps, + ) => ( +
+ TechDocs - Rank: {props.rank} - Line clamp: {props.lineClamp} +
+ ); const TechDocsSearchResultItemExtension = createSearchResultListItemExtension({ id: 'techdocs', at: 'plugin.search.page/items', + configSchema: createSchemaFromZod(z => + z.object({ + noTrack: z.boolean().default(true), + lineClamp: z.number().default(5), + }), + ), predicate: result => result.type === 'techdocs', - component: async () => TechDocsSearchResultItemComponent, + component: + async ({ config }) => + props => + , }); const ExploreSearchResultItemComponent = ( @@ -129,7 +146,6 @@ describe('createSearchResultListItemExtension', () => { key={index} rank={result.rank} result={result.document} - noTrack /> ); })} @@ -153,7 +169,20 @@ describe('createSearchResultListItemExtension', () => { const app = createApp({ plugins: [SearchPlugin], - configLoader: async () => new MockConfigApi({}), + configLoader: async () => + new MockConfigApi({ + app: { + extensions: [ + { + 'plugin.search.result.item.techdocs': { + config: { + lineClamp: 3, + }, + }, + }, + ], + }, + }), }); render(app.createRoot()); @@ -161,7 +190,9 @@ describe('createSearchResultListItemExtension', () => { expect(await screen.findByText(/Search Page/)).toBeInTheDocument(); expect( - await screen.findByText(/TechDocs - Rank: 1/, { exact: false }), + await screen.findByText(/TechDocs - Rank: 1 - Line clamp: 3/, { + exact: false, + }), ).toBeInTheDocument(); expect( diff --git a/plugins/search-react/src/wiring/createSearchResultListItemExtension.tsx b/plugins/search-react/src/wiring/createSearchResultListItemExtension.tsx index 4ad0e54e1f..b276c5e60a 100644 --- a/plugins/search-react/src/wiring/createSearchResultListItemExtension.tsx +++ b/plugins/search-react/src/wiring/createSearchResultListItemExtension.tsx @@ -20,8 +20,10 @@ import { ListItemProps } from '@material-ui/core'; import { ExtensionBoundary, + PortableSchema, createExtension, createExtensionDataRef, + createSchemaFromZod, } from '@backstage/frontend-plugin-api'; import { Progress } from '@backstage/core-components'; import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; @@ -47,7 +49,9 @@ export const searchResultItemExtensionData = createExtensionDataRef<{ }>('plugin.search.result.item.data'); /** @alpha */ -export type SearchResultItemExtensionOptions = { +export type SearchResultItemExtensionOptions< + TConfig extends { noTrack?: boolean }, +> = { /** * The extension id. */ @@ -56,10 +60,16 @@ export type SearchResultItemExtensionOptions = { * The extension attachment point (e.g., search modal or page). */ at: string; + /** + * Optional extension config schema. + */ + configSchema?: PortableSchema; /** * The extension component. */ - component: () => Promise; + component: (options: { + config: TConfig; + }) => Promise; /** * When an extension defines a predicate, it returns true if the result should be rendered by that extension. * Defaults to a predicate that returns true, which means it renders all sorts of results. @@ -68,25 +78,35 @@ export type SearchResultItemExtensionOptions = { }; /** @alpha */ -export type BaseSearchResultListItemProps = { +export type BaseSearchResultListItemProps = T & { rank?: number; result?: SearchDocument; - noTrack?: boolean; } & Omit; /** @alpha */ -export const createSearchResultListItemExtension = ( - options: SearchResultItemExtensionOptions, -) => - createExtension({ +export function createSearchResultListItemExtension< + TConfig extends { noTrack?: boolean }, +>(options: SearchResultItemExtensionOptions) { + const configSchema = + 'configSchema' in options + ? options.configSchema + : (createSchemaFromZod(z => + z.object({ + noTrack: z.boolean().default(true), + }), + ) as PortableSchema); + return createExtension({ id: `plugin.search.result.item.${options.id}`, at: options.at, + configSchema, output: { item: searchResultItemExtensionData, }, - factory({ bind, source }) { + factory({ bind, config, source }) { const LazyComponent = lazy(() => - options.component().then(component => ({ default: component })), + options + .component({ config }) + .then(component => ({ default: component })), ) as unknown as SearchResultItemExtensionComponent; bind({ @@ -98,7 +118,7 @@ export const createSearchResultListItemExtension = ( @@ -109,3 +129,4 @@ export const createSearchResultListItemExtension = ( }); }, }); +}