Lab - Add an Overview Page

Plan: B2B Developer

Lesson 12 of 25 · 30 min

Introduction

In this exercise, you’ll add a brand new “Overview” page to the Buyer Portal, and in the process you’ll practice routing setup, permissions configuration, and navigation between pages.

In this lab, you will:

  • Add simple route permissions configuration
  • Configure a new page route
  • Make content conditional on permissions
  • Navigate in response to user interaction

Setup

This exercise continues where you left off previously. Make sure the dev server is started in your Buyer Portal project:

yarn dev

Remember!

These exercises will proceed from a project you previously set up from the lab repository, which contains necessary boilerplate code.

Script Manager in your store control panel must contain appropriate header/footer scripts to load the Buyer Portal from your local environment on the appropriate port.

Revisit the initial setup lab if necessary to review the details.

Exercise Reference Copy

If it’s helpful to compare your own code as you go, you can clone or download the completed version of this labs as a reference.

Overview Page Lab Snapshot

Step 1: Configure the Route

Remember, all file paths referenced in these exercises are relative to apps/storefront/src in your Buyer Portal project.

The project you’ve copied already has a very basic component for the page you’ll be setting up, at pages/Overview/index.tsx. Before the Overview page will show up in the frontend, you must map this component to a route and provide several other pieces of configuration for that route.

The route configuration in question will make reference to permissions, so we’ll need to set those up first.

  1. Open shared/routes/config.ts. This file contains route-related permissions configuration.
  2. Modify legacyPermissions to add a new item for Overview.
config.ts
const legacyPermissions = {
dashboardPermissions: [...],
overviewPermissions: [
CustomerRole.SUPER_ADMIN,
CustomerRole.SUPER_ADMIN_BEFORE_AGENCY,
CustomerRole.ADMIN,
CustomerRole.SENIOR_BUYER,
CustomerRole.JUNIOR_BUYER,
CustomerRole.CUSTOM_ROLE,
CustomerRole.B2C,
CustomerRole.GUEST,
],
ordersPermissions: [
CustomerRole.SUPER_ADMIN,
...
],
...
};

The order of the items in this config object does not strictly matter. Notice that the array’s values are supplied by a CustomerRole enumeration. Our intention is that the Overview page is accessible by all users, so we’ve mimicked ordersPermissions, which lists all possible roles.

  1. Modify newPermissions to similarly add a line for Overview.
config.ts
const newPermissions = {
overviewPermissionCodes: b2bPermissionsMap.getShoppingListPermission,
ordersPermissionCodes: b2bPermissionsMap.getOrderPermission,
...
};

Again, we are mimicking the loosest restrictions with these permissions, so in this case we have duplicated getShoppingListPermission.

On Permissions

config.ts contains two different types of permissions:

  • Role-based permissions are based on a user’s role, such as Senior Buyer, Junior Buyer, or custom roles. legacyPermissions maps various roles to a route key.
  • Permission “codes” are more granular, based on the specific permissions that are granted to roles in the B2B Edition backend. newPermissions maps a permission code to a route key.

You may notice that the values in newPermissions reference values from a b2bPermissionsMap containing the various permission strings (such as “get_shopping_lists”, “create_quote”, “get_invoice_detail”, etc). The built-in values here correspond with the permissions that exist in B2B Edition by default and are assigned to the various built-in roles.

We might consider defining our own custom permission with the B2B Edition API and incorporating that permission’s code into the map. To keep things simple, we’ve instead re-used an existing permission (getShoppingListPermission or “get_shopping_lists”). Users in our store who have the “get shopping lists” permission will also be able to access the Overview page.

Routing Config

Now that we have permissions, we can wire up the new route by modifying a couple of key files.

  1. Open shared/routeList.ts, which defines all routes with their basic config.
  2. Update routeList with a new Overview item.
routeList.ts
export const routeList: (BuyerPortalRoute | RouteItem)[] = [
{
path: '/dashboard',
...
},
{
path: '/overview',
name: 'Overview',
wsKey: 'router-overview',
isMenuItem: true,
permissions: overviewPermissions,
permissionCodes: overviewPermissionCodes,
isTokenLogin: true,
idLang: 'global.navMenu.overview',
},
{
path: '/orders',
...
},
...
];

This config includes the following:

  • path defines the hash portion of the URL path that will correspond with the page.
  • isMenuItem declares that this page should appear in the nav menu.
  • permissions and permissionCodes apply the permissions config we prepared.
  • isTokenLogin declares that a user must be logged in to access this page.
  • idLang identifies the locale string key used to look up the locale-specific page title. (The “Overview” page title has already been included in the locale strings in lib/lang/locales/en.json.)

The only thing we haven’t taken care of with this config is actually mapping the “/overview” route to the page component. This is handled in a separate file.

  1. Open shared/routes/index.tsx and add a “lazy” import of the correct component, as well as the required config in routesMap.
routes/index.tsx
...
const Overview = lazy(() => import('@/pages/Overview'));
const AccountSetting = lazy(() => import('@/pages/AccountSetting'));
...
const routesMap: Record<string, LazyExoticComponent<(props: PageProps) => ReactElement>> = {
'/overview': Overview,
'/dashboard': Dashboard,
...
};

We’ve actually completed all the config required for our page to be displayed. One last change we’ll make is to add a new constant to provide a clean and consistent way to reference this route (such as with window.b2b.utils.openPage).

  1. Open constants/index.ts and add an OVERVIEW constant to HeadlessRoutes.
constants/index.ts
export enum HeadlessRoutes {
...
DASHBOARD = '/dashboard',
OVERVIEW = '/overview',
ORDERS = '/orders',
...
}

Any company user should now see the “Overview” item in the Buyer Portal nav menu. The page is currently very bare, containing only placeholders for the sections we’ll be building.

Overview page with placeholders

What About React Router?

You might have noticed that the code we’ve added didn’t directly deal with React Router. The Buyer Portal manages information about the various routes via the configuration we’ve seen, and we don’t need to worry about updating the code that iterates over this config to create the routes themselves.

Here’s a brief overview of the wiring involved:

  • App.tsx wraps the entire layout in HashRouter from the react-router-dom package.
  • components/layout/B3RenderRouter.tsx handles the heavy lifting:
    • The Routes and Route components from react-router-dom are used to declare the routes.
    • getAllowedRoutes is used to fetch the config of all routes the user has permissions for.
    • The list of routes is looped, with a Route component rendered for each.

Step 2: Change the Default Page

Our remaining steps for this exercise won’t be as involved as the router config. First, an “Overview” page probably makes the most sense as the default page when a user first opens the Buyer Portal.

The logic for determining this landing page is contained in a utility function.

  1. Open utils/b3CheckPermissions/b2bPermissionPath.ts and add a conditional to the top of b2bJumpPath.
b2bPermissionPath.ts
export const b2bJumpPath = (role: number): string => {
if (hasPermission('getShoppingListPermission')) {
return PATH_ROUTES.OVERVIEW;
}
if (role === CustomerRole.SUPER_ADMIN) {
...
}
...
};

The very first condition in the utility function will now return the “Overview” route as the default route for any user with the shopping list permission (the same permission we tied our route config to).

Realistically, this condition should apply to any of the built-in roles, but we’ll leave most of the function’s remaining logic in place, since we’ve tied our own condition explicitly to a permission code. The only clean-up we need to do is to get rid of the existing condition relying on the shopping list permission; this conditional is redundant now and will never be reached.

  1. Remove the hasPermission('getShoppingListPermission') conditional block later in the function.
b2bPermissionPath.ts
export const b2bJumpPath = (role: number): string => {
...
/* REMOVE these lines
if (hasPermission('getShoppingListPermission')) {
return PATH_ROUTES.SHOPPING_LISTS;
}
*/
return PATH_ROUTES.ACCOUNT_SETTINGS;
};

Try closing the Buyer Portal and then re-opening it with the “Account” nav link. You should find that Overview is presented immediately.

Step 3: Restrict Content by Permissions

Remember that we’ve configured our route permissions such that anyone allowed to view shopping lists can access the Overview page itself.

As indicated by the page’s current placeholders, however, part of the page’s content will include information about recent orders. Our shopping list permission does not guarantee that a given user is allowed to view orders! What we need in this case is to compare permissions directly in our component logic to make this content conditional.

  1. Open pages/Overview/index.tsx, which contains the main page component.
  2. Add the following immediately before the returned JSX.
Overview/index.tsx
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
const allowOrders = validatePermissionWithComparisonType({
code: newPermissions.ordersPermissionCodes,
level: permissionLevels.COMPANY,
containOrEqual: 'contain',
});
return (
...
);
}

The key utility function here is validatePermissionWithComparisonType. The params we’ve passed to this function validate that the current user’s permissions contain the code we’ve specified (ordersPermissionCodes, from the same config we’ve already seen). The level param specifies that we’re validating the permission to view all company orders, not just a user’s own orders.

  1. In the component’s JSX, wrap the “Recent Orders” placeholder in a conditional.
Overview/index.tsx
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
return (
<>
<Grid
...
>
...
{allowOrders && (
<Grid
...
>
<h3>Recent Orders Placeholder</h3>
</Grid>
)}
</Grid>
</>
);
}

Try logging into your storefront as a user with the “Junior Buyer” role. With the new permission check in place, the “Recent Orders” placeholder should no longer appear.

Don’t forget to log in again as a user with full admin permissions before subsequent labs!

Step 4: Navigate Between Pages

In our last step for this exercise, we’ll add a link to another Buyer Portal page. Our Overview page will show only limited information about recent orders (and later other data). We’ll provide a link taking users straight to the main orders page.

When B3RenderRouter renders each route’s page component, the function setOpenPage is passed as a prop. Navigation is as easy as accepting this prop and making use of the function.

The Overview component already accepts the appropriate prop:

Overview/index.tsx
interface OverviewProps {
setOpenPage: SetOpenPage;
}
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
}

Let’s utilize setOpenPage in an appropriate event handler.

  1. Open pages/Overview/index.tsx and add a Button component.
Overview/index.tsx
export default function Overview({
setOpenPage,
}: OverviewProps) {
...
return (
<>
<Grid
...
>
...
{allowOrders && (
<Grid
...
>
<h3>Recent Orders Placeholder</h3>
<Button onClick={() => setOpenPage({ isOpen: true, openUrl: HeadlessRoutes.COMPANY_ORDERS })}>
{b3Lang('overview.allOrders')}
</Button>
</Grid>
)}
</Grid>
</>
);
}

Note the params passed in the argument to setOpenPage:

  • We’re making use of a HeadlessRoutes constant for the target page as the value of openUrl. We can also manually pass relative paths (such as if the path contains a dynamic slug), but relying on these constants when possible is the safest method.
  • It might seem odd to pass an isOpen value, but note that setOpenPage can be used to close the Buyer Portal UI by passing isOpen: false.

Visit the Overview page again and verify that your “All Orders” link functions.

All Orders link

While not the focus of this exercise, the following are notable about our code:

  • The Button component, and the Grid component already present in the page, are part of the Material UI library, which we’ll explore next.
  • The b3Lang function renders a localized string based on the JSON files in lib/lang/locales.

Full Exercise Code

Taking It Further

  • Utilize the B2B REST Management API to create a new permission called “overview”. Then add this permission code to the master list and make the appropriate changes to incorporate this custom permission. Create a custom role with this permission in the control panel to test out your logic.
  • Imagine an additional React state value (like the setOpenPage function) that might be useful for all pages to access. Identify the appropriate place to initialize that state value and pass it as a prop to all route components.

Resources