app-react: keep nav rest results in sync
Make nav rest results stay live when additional items are taken later in the same render, which lets app nav layouts place specific items after collecting the remaining sidebar entries. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -31,36 +31,72 @@ function mockNode(id: string): AppNode {
|
||||
|
||||
function mockNavItems(items: NavContentNavItem[]): NavContentNavItems {
|
||||
const taken = new Set<string>();
|
||||
let restItems: NavContentNavItem[] | undefined;
|
||||
return {
|
||||
take(id: string) {
|
||||
const item = items.find(i => i.node.spec.id === id);
|
||||
if (item) {
|
||||
if (item && !taken.has(id)) {
|
||||
taken.add(id);
|
||||
if (restItems) {
|
||||
const index = restItems.findIndex(
|
||||
restItem => restItem.node.spec.id === id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
restItems.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return item;
|
||||
},
|
||||
rest: () => items.filter(i => !taken.has(i.node.spec.id)),
|
||||
rest: () => {
|
||||
if (!restItems) {
|
||||
restItems = items.filter(i => !taken.has(i.node.spec.id));
|
||||
}
|
||||
return restItems;
|
||||
},
|
||||
clone() {
|
||||
return mockNavItems(items);
|
||||
},
|
||||
withComponent(Component: (props: NavContentNavItem) => JSX.Element) {
|
||||
let renderedItems: JSX.Element[] | undefined;
|
||||
return {
|
||||
take: (id: string) => {
|
||||
const item = items.find(i => i.node.spec.id === id);
|
||||
if (item) {
|
||||
if (item && !taken.has(id)) {
|
||||
taken.add(id);
|
||||
return <Component {...item} />;
|
||||
if (restItems) {
|
||||
const index = restItems.findIndex(
|
||||
restItem => restItem.node.spec.id === id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
restItems.splice(index, 1);
|
||||
}
|
||||
}
|
||||
if (renderedItems) {
|
||||
const index = renderedItems.findIndex(
|
||||
renderedItem => renderedItem.key === item.node.spec.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
renderedItems.splice(index, 1);
|
||||
}
|
||||
}
|
||||
return <Component key={item.node.spec.id} {...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));
|
||||
if (!restItems) {
|
||||
restItems = items.filter(i => !taken.has(i.node.spec.id));
|
||||
}
|
||||
return remaining.map(item => (
|
||||
<Component key={item.node.spec.id} {...item} />
|
||||
));
|
||||
if (!renderedItems) {
|
||||
if (options?.sortBy === 'title') {
|
||||
restItems.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
renderedItems = restItems.map(item => (
|
||||
<Component key={item.node.spec.id} {...item} />
|
||||
));
|
||||
}
|
||||
return renderedItems;
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -267,4 +303,61 @@ describe('NavContentBlueprint', () => {
|
||||
expect(docsLink).toBeInTheDocument();
|
||||
expect(docsLink.closest('nav')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should keep the rendered rest array in sync with later take calls', () => {
|
||||
const items: NavContentNavItem[] = [
|
||||
{
|
||||
node: mockNode('page:catalog'),
|
||||
href: '/catalog',
|
||||
title: 'Catalog',
|
||||
icon: <span>catalog</span>,
|
||||
routeRef,
|
||||
},
|
||||
{
|
||||
node: mockNode('page:devtools'),
|
||||
href: '/devtools',
|
||||
title: 'DevTools',
|
||||
icon: <span>devtools</span>,
|
||||
routeRef,
|
||||
},
|
||||
{
|
||||
node: mockNode('page:user-settings'),
|
||||
href: '/settings',
|
||||
title: 'Settings',
|
||||
icon: <span>settings</span>,
|
||||
routeRef,
|
||||
},
|
||||
];
|
||||
|
||||
const extension = NavContentBlueprint.make({
|
||||
name: 'test',
|
||||
params: {
|
||||
component: ({ navItems }) => {
|
||||
const nav = navItems.withComponent(item => (
|
||||
<a href={item.href}>{item.title}</a>
|
||||
));
|
||||
const rest = nav.rest({ sortBy: 'title' });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav>{rest}</nav>
|
||||
<aside>{nav.take('page:devtools')}</aside>
|
||||
<footer>{nav.take('page:user-settings')}</footer>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
const Component = tester.get(NavContentBlueprint.dataRefs.component);
|
||||
|
||||
render(<Component navItems={mockNavItems(items)} items={[]} />);
|
||||
|
||||
expect(screen.getByText('Catalog').closest('nav')).toBeTruthy();
|
||||
expect(screen.queryByText('DevTools')?.closest('nav')).toBeFalsy();
|
||||
expect(screen.queryByText('Settings')?.closest('nav')).toBeFalsy();
|
||||
expect(screen.getByText('DevTools').closest('aside')).toBeTruthy();
|
||||
expect(screen.getByText('Settings').closest('footer')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,7 +53,12 @@ export interface NavContentNavItem {
|
||||
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. */
|
||||
/**
|
||||
* Render all remaining items not yet taken, optionally sorted.
|
||||
*
|
||||
* The returned array is live and is updated when later `take` calls remove
|
||||
* items from the collection.
|
||||
*/
|
||||
rest(options?: { sortBy?: 'title' }): JSX.Element[];
|
||||
}
|
||||
|
||||
@@ -66,7 +71,12 @@ export interface NavContentNavItemsWithComponent {
|
||||
export interface NavContentNavItems {
|
||||
/** Take an item by extension ID, removing it from the collection. */
|
||||
take(id: string): NavContentNavItem | undefined;
|
||||
/** All items not yet taken. */
|
||||
/**
|
||||
* All items not yet taken.
|
||||
*
|
||||
* The returned array is live and is updated when later `take` calls remove
|
||||
* items from the collection.
|
||||
*/
|
||||
rest(): NavContentNavItem[];
|
||||
/** Create a copy of the collection preserving the current taken state. */
|
||||
clone(): NavContentNavItems;
|
||||
|
||||
Reference in New Issue
Block a user