app-react: API cleanup and component utility for nav items
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -108,11 +108,11 @@ export interface NavContentComponentProps {
|
||||
to: string;
|
||||
text: string;
|
||||
}>;
|
||||
navItems: NavItems;
|
||||
navItems: NavContentNavItems;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface NavItem {
|
||||
export interface NavContentNavItem {
|
||||
href: string;
|
||||
icon: IconElement;
|
||||
node: AppNode;
|
||||
@@ -121,10 +121,19 @@ export interface NavItem {
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface NavItems {
|
||||
clone(): NavItems;
|
||||
rest(): NavItem[];
|
||||
take(id: string): NavItem | undefined;
|
||||
export interface NavContentNavItems {
|
||||
clone(): NavContentNavItems;
|
||||
rest(): NavContentNavItem[];
|
||||
take(id: string): NavContentNavItem | undefined;
|
||||
withComponent(
|
||||
Component: ComponentType<NavContentNavItem>,
|
||||
): NavContentNavItemsWithComponent;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface NavContentNavItemsWithComponent {
|
||||
rest(options?: { sortBy?: 'title' }): JSX.Element[];
|
||||
take(id: string): JSX.Element | null;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -15,8 +15,13 @@
|
||||
*/
|
||||
|
||||
import { AppNode, createRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { NavContentBlueprint, NavItem, NavItems } from './NavContentBlueprint';
|
||||
import {
|
||||
NavContentBlueprint,
|
||||
NavContentNavItem,
|
||||
NavContentNavItems,
|
||||
} from './NavContentBlueprint';
|
||||
import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
|
||||
@@ -24,13 +29,41 @@ function mockNode(id: string): AppNode {
|
||||
return { spec: { id } } as AppNode;
|
||||
}
|
||||
|
||||
function mockNavItems(items: NavItem[]): NavItems {
|
||||
function mockNavItems(items: NavContentNavItem[]): NavContentNavItems {
|
||||
const taken = new Set<string>();
|
||||
return {
|
||||
take: () => undefined,
|
||||
rest: () => items,
|
||||
take(id: string) {
|
||||
const item = items.find(i => i.node.spec.id === id);
|
||||
if (item) {
|
||||
taken.add(id);
|
||||
}
|
||||
return item;
|
||||
},
|
||||
rest: () => items.filter(i => !taken.has(i.node.spec.id)),
|
||||
clone() {
|
||||
return mockNavItems(items);
|
||||
},
|
||||
withComponent(Component: (props: NavContentNavItem) => JSX.Element) {
|
||||
return {
|
||||
take: (id: string) => {
|
||||
const item = items.find(i => i.node.spec.id === id);
|
||||
if (item) {
|
||||
taken.add(id);
|
||||
return <Component {...item} />;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
rest: (options?: { sortBy?: 'title' }) => {
|
||||
const remaining = items.filter(i => !taken.has(i.node.spec.id));
|
||||
if (options?.sortBy === 'title') {
|
||||
remaining.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
return remaining.map(item => (
|
||||
<Component key={item.node.spec.id} {...item} />
|
||||
));
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,7 +144,7 @@ describe('NavContentBlueprint', () => {
|
||||
});
|
||||
|
||||
it('should return a valid component with navItems', () => {
|
||||
const items: NavItem[] = [
|
||||
const items: NavContentNavItem[] = [
|
||||
{
|
||||
node: mockNode('page:home'),
|
||||
href: '/',
|
||||
@@ -173,4 +206,64 @@ describe('NavContentBlueprint', () => {
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
it('should support withComponent for take and rest', () => {
|
||||
const items: NavContentNavItem[] = [
|
||||
{
|
||||
node: mockNode('page:home'),
|
||||
href: '/',
|
||||
title: 'Home',
|
||||
icon: <span>home</span>,
|
||||
routeRef,
|
||||
},
|
||||
{
|
||||
node: mockNode('page:catalog'),
|
||||
href: '/catalog',
|
||||
title: 'Catalog',
|
||||
icon: <span>catalog</span>,
|
||||
routeRef,
|
||||
},
|
||||
{
|
||||
node: mockNode('page:docs'),
|
||||
href: '/docs',
|
||||
title: 'Docs',
|
||||
icon: <span>docs</span>,
|
||||
routeRef,
|
||||
},
|
||||
];
|
||||
|
||||
const extension = NavContentBlueprint.make({
|
||||
name: 'test',
|
||||
params: {
|
||||
component: ({ navItems }) => {
|
||||
const nav = navItems.withComponent(item => (
|
||||
<a href={item.href}>{item.title}</a>
|
||||
));
|
||||
return (
|
||||
<div>
|
||||
<header>{nav.take('page:home')}</header>
|
||||
<nav>{nav.rest()}</nav>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
const Component = tester.get(NavContentBlueprint.dataRefs.component);
|
||||
|
||||
render(<Component navItems={mockNavItems(items)} items={[]} />);
|
||||
|
||||
const homeLink = screen.getByText('Home');
|
||||
expect(homeLink).toBeInTheDocument();
|
||||
expect(homeLink.closest('header')).toBeTruthy();
|
||||
|
||||
const catalogLink = screen.getByText('Catalog');
|
||||
expect(catalogLink).toBeInTheDocument();
|
||||
expect(catalogLink.closest('nav')).toBeTruthy();
|
||||
|
||||
const docsLink = screen.getByText('Docs');
|
||||
expect(docsLink).toBeInTheDocument();
|
||||
expect(docsLink.closest('nav')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import {
|
||||
AppNode,
|
||||
IconComponent,
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface NavItem {
|
||||
export interface NavContentNavItem {
|
||||
/** The app node of the page extension that this nav item points to */
|
||||
node: AppNode;
|
||||
/** The resolved route path */
|
||||
@@ -43,19 +44,36 @@ export interface NavItem {
|
||||
routeRef: RouteRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* A pre-bound renderer that wraps {@link NavContentNavItems} with a component,
|
||||
* so that `take` and `rest` return rendered elements directly.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface NavContentNavItemsWithComponent {
|
||||
/** Render and take a specific item by extension ID. Returns null if not found. */
|
||||
take(id: string): JSX.Element | null;
|
||||
/** Render all remaining items not yet taken, optionally sorted. */
|
||||
rest(options?: { sortBy?: 'title' }): JSX.Element[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of nav items that supports picking specific items by ID
|
||||
* and retrieving whatever remains. Created fresh for each render.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface NavItems {
|
||||
export interface NavContentNavItems {
|
||||
/** Take an item by extension ID, removing it from the collection. */
|
||||
take(id: string): NavItem | undefined;
|
||||
take(id: string): NavContentNavItem | undefined;
|
||||
/** All items not yet taken. */
|
||||
rest(): NavItem[];
|
||||
rest(): NavContentNavItem[];
|
||||
/** Create a copy of the collection preserving the current taken state. */
|
||||
clone(): NavItems;
|
||||
clone(): NavContentNavItems;
|
||||
/** Create a renderer that wraps take/rest to return pre-rendered elements. */
|
||||
withComponent(
|
||||
Component: ComponentType<NavContentNavItem>,
|
||||
): NavContentNavItemsWithComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +86,7 @@ export interface NavContentComponentProps {
|
||||
* Nav items auto-discovered from page extensions, with take/rest semantics
|
||||
* for placing specific items in specific positions.
|
||||
*/
|
||||
navItems: NavItems;
|
||||
navItems: NavContentNavItems;
|
||||
|
||||
/**
|
||||
* Flat list of nav items for simple rendering. Use `navItems` for more
|
||||
|
||||
@@ -20,8 +20,9 @@ export { NavContentBlueprint } from './NavContentBlueprint';
|
||||
export type {
|
||||
NavContentComponent,
|
||||
NavContentComponentProps,
|
||||
NavItem,
|
||||
NavItems,
|
||||
NavContentNavItem,
|
||||
NavContentNavItemsWithComponent,
|
||||
NavContentNavItems,
|
||||
} from './NavContentBlueprint';
|
||||
export { RouterBlueprint } from './RouterBlueprint';
|
||||
export { SignInPageBlueprint } from './SignInPageBlueprint';
|
||||
|
||||
Reference in New Issue
Block a user