Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: https://github.com/smapiot/piral/issues/610 dynamic Breadcru… #614

Merged
merged 7 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions src/plugins/piral-breadcrumbs/src/Breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import create from 'zustand';
import { render } from '@testing-library/react';
import { StateContext } from 'piral-core';
import { Breadcrumbs } from './Breadcrumbs';
import { useRouteMatch } from 'react-router';

const MockBcContainer: React.FC<any> = ({ children }) => <div role="container">{children}</div>;
MockBcContainer.displayName = 'MockBcContainer';
Expand All @@ -16,9 +17,7 @@ jest.mock('react-router', () => ({
pathname: '/example',
};
},
useRouteMatch() {
return {};
},
useRouteMatch: jest.fn(() => ({})),
}));

function createMockContainer(breadcrumbs = {}) {
Expand Down Expand Up @@ -57,8 +56,8 @@ describe('Piral-Breadcrumb Container component', () => {
<Breadcrumbs />
</StateContext.Provider>,
);
expect(node.getAllByRole("container").length).toBe(1);
expect(node.queryByRole("dialog")).toBe(null);
expect(node.getAllByRole('container').length).toBe(1);
expect(node.queryByRole('dialog')).toBe(null);
});

it('uses container and item for each breadcrumb', () => {
Expand Down Expand Up @@ -92,7 +91,33 @@ describe('Piral-Breadcrumb Container component', () => {
<Breadcrumbs />
</StateContext.Provider>,
);
expect(node.getAllByRole("container").length).toBe(1);
expect(node.getAllByRole("dialog").length).toBe(2);
expect(node.getAllByRole('container').length).toBe(1);
expect(node.getAllByRole('dialog').length).toBe(2);
});

it('uses container and dynamic title function and computes path without wildcards', () => {
(useRouteMatch as any).mockReturnValueOnce({ params: { example: 'replacedWildcard' } });

const { context } = createMockContainer({
example: {
matcher: /^\/example$/,
settings: {
path: '/:example*',
title: ({ path }) => {
return path;
},
parent: '/',
},
},
});
const node = render(
<StateContext.Provider value={context}>
<Breadcrumbs />
</StateContext.Provider>,
);

expect(node.getAllByRole('container').length).toBe(1);
expect(node.getAllByRole('dialog').length).toBe(1);
expect(node.getAllByRole('container')[0].innerHTML).toBe('<div role="dialog">/replacedWildcard</div>');
});
});
25 changes: 23 additions & 2 deletions src/plugins/piral-breadcrumbs/src/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ import * as React from 'react';
import { useLocation, useRouteMatch } from 'react-router';
import { PiralBreadcrumbsContainer, PiralBreadcrumbItem } from './components';
import { useBreadcrumbs } from './useBreadcrumbs';
import { BreadcrumbSettings } from './types';
import { Location } from 'history';

function getContent(
title: BreadcrumbSettings['title'],
location: Location,
path: string,
params: Record<string, string>,
) {
if (typeof title === 'function') {
return title({ location, path, params });
}

return title;
}

export const Breadcrumbs: React.FC = () => {
const location = useLocation();
Expand All @@ -14,11 +29,17 @@ export const Breadcrumbs: React.FC = () => {
const { title, path, ...props } = settings;
const key = `bc_${i}_${settings.path}`;
const current = i === currentIndex;
const computedPath = path.replace(/:([A-Za-z0-9_]+)/g, (s, id) => params[id] ?? s);
const computedPath = path.replace(/:(([A-Za-z0-9_]+)\*?)/g, (s, _, id) => {
if (id in params) {
return params[id] || '';
}

return s;
});

return (
<PiralBreadcrumbItem key={key} current={current} path={computedPath} {...props}>
{title}
{getContent(title, location, computedPath, params)}
</PiralBreadcrumbItem>
);
});
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/piral-breadcrumbs/src/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('Breadcrumbs Actions Module', () => {
registerBreadcrumbs(ctx, {
foo: 10 as any,
});
expect((state.getState())).toEqual({
expect(state.getState()).toEqual({
foo: 5,
registry: {
foo: 5,
Expand All @@ -26,7 +26,7 @@ describe('Breadcrumbs Actions Module', () => {
},
});
unregisterBreadcrumbs(ctx, ['foo']);
expect((state.getState())).toEqual({
expect(state.getState()).toEqual({
foo: 5,
registry: {
foo: 5,
Expand Down
17 changes: 17 additions & 0 deletions src/plugins/piral-breadcrumbs/src/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,21 @@ describe('Create Breadcrumb API Extensions', () => {
const ids = Object.keys(container.context.registerBreadcrumbs.mock.calls[0][0]);
expect(container.context.unregisterBreadcrumbs.mock.calls[0][0]).toEqual(ids);
});

it('createBreadcrumsApi can use dynamic function as breadcrumb title', () => {
const container = createMockContainer();
container.context.registerBreadcrumbs = jest.fn();
container.context.unregisterBreadcrumbs = jest.fn();
const api = createApi(container);
const dispose = api.registerBreadcrumb({
title: ({ path }) => path,
FlorianRappl marked this conversation as resolved.
Show resolved Hide resolved
path: '/example',
});
expect(container.context.registerBreadcrumbs).toHaveBeenCalledTimes(1);
expect(container.context.unregisterBreadcrumbs).toHaveBeenCalledTimes(0);
dispose();
expect(container.context.registerBreadcrumbs).toHaveBeenCalledTimes(1);
const ids = Object.keys(container.context.registerBreadcrumbs.mock.calls[0][0]);
expect(container.context.unregisterBreadcrumbs.mock.calls[0][0]).toEqual(ids);
});
});
15 changes: 13 additions & 2 deletions src/plugins/piral-breadcrumbs/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ComponentType, ReactNode } from 'react';
import type { Dict, BaseRegistration, RegistrationDisposer } from 'piral-core';
import type { Location } from 'history';

declare module 'piral-core/lib/types/custom' {
interface PiletCustomApi extends PiletBreadcrumbsApi {}
Expand All @@ -12,6 +13,7 @@ declare module 'piral-core/lib/types/custom' {
* @param values The breadcrumbs to register.
*/
registerBreadcrumbs(values: Dict<BreadcrumbRegistration>): void;

/**
* Unregisters an existing breadcrumb.
* @param name The name of the breadcrumb to be removed.
Expand Down Expand Up @@ -58,6 +60,12 @@ export interface BreadcrumbItemProps extends Omit<BreadcrumbSettings, 'title'> {

export interface PiralCustomBreadcrumbSettings {}

export interface BreadcrumbTitleParams {
location: Location;
path: string;
params: Record<string, string>;
}

export interface BreadcrumbSettings extends PiralCustomBreadcrumbSettings {
/**
* Gets the path of breadcrumb for navigation purposes.
Expand All @@ -83,7 +91,7 @@ export interface BreadcrumbSettings extends PiralCustomBreadcrumbSettings {
/**
* The title of the breadcrumb.
*/
title: ReactNode;
title: ReactNode | ((params: BreadcrumbTitleParams) => ReactNode);
}

export interface BreadcrumbRegistration extends BaseRegistration {
Expand All @@ -96,19 +104,22 @@ export interface PiletBreadcrumbsApi {
* Registers a set of breadcrumbs.
* @param values The different breadcrumb settings.
*/
registerBreadcrumbs(values: Array<{ name?: string; } & BreadcrumbSettings>): RegistrationDisposer;
registerBreadcrumbs(values: Array<{ name?: string } & BreadcrumbSettings>): RegistrationDisposer;

/**
* Registers a breadcrumb with the provided settings.
* @param settings The settings for configuring the breadcrumb.
*/
registerBreadcrumb(settings: BreadcrumbSettings): RegistrationDisposer;

/**
* Registers a named breadcrumb with the provided settings.
* The name has to be unique within the current pilet.
* @param name The name of the breadcrumb.
* @param settings The settings for configuring the breadcrumb.
*/
registerBreadcrumb(name: string, settings: BreadcrumbSettings): RegistrationDisposer;

/**
* Unregisters a breadcrumb known by the given name.
* Only previously registered tiles can be unregistered.
Expand Down
6 changes: 5 additions & 1 deletion src/samples/sample-piral/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { LayoutProps, Menu, Notifications, Modals, Languages } from 'piral';
import { Search } from 'piral-search';
import { MenuToggle } from './MenuToggle';
import { User } from './User';
import { Breadcrumbs } from 'piral-breadcrumbs';

export const Layout: React.FC<LayoutProps> = ({ children }) => (
<div className="app-container">
Expand All @@ -24,7 +25,10 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => (
<Languages />
<User />
</div>
<div className="app-content">{children}</div>
<div className="app-content">
<Breadcrumbs />
{children}
</div>
<div className="app-footer">
<Menu type="footer" />
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/samples/sample-piral/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { createRoot } from 'react-dom/client';
import { createInstance, createStandardApi, getUserLocale, Piral, setupLocalizer } from 'piral';
import { createAuthApi } from 'piral-auth';
import { createSearchApi } from 'piral-search';
import { createBreadcrumbsApi } from 'piral-breadcrumbs';
import { setupFooter, setupMenu } from './parts';
import { layout, errors } from './layout';

const instance = createInstance({
plugins: [
createAuthApi(),
createSearchApi(),
createBreadcrumbsApi(),
FlorianRappl marked this conversation as resolved.
Show resolved Hide resolved
...createStandardApi({
locale: setupLocalizer({
language: getUserLocale,
Expand All @@ -37,3 +39,5 @@ const instance = createInstance({

const root = createRoot(document.querySelector('#app'));
root.render(<Piral instance={instance} />);