Broadleaf Microservices

Considerations for New Admin Components

The following sections detail some things to consider when making custom components for the admin client.

Importing from the component library

@broadleaf/admin-components provides components, context, utilities, and more to help enable developers to customize the admin client. It is broken down into the following modules:

  • AdminApp: This is the main component representing the app itself

  • AdminProvider: The typical entrypoint component that renders the necessary context providers for the application

  • caches: Incldues the caches used such as for auth and tenant state and component routing

  • components: Includes the main components necessary for rendering custom components

  • contexts: Includes the various React Contexts created and used by the application

  • hooks: Includes key hooks for component functionality

  • metadata: Includes the default metadata-driven form components available out-of-box

  • services: Includes key services use throughout the admin such as the ComponentRegistrar

  • utils: Includes commonly used utilities such as axios and RequestUtils

  • unstable: This module contains a variety of additional exports that should be treated with caution because they may be subject to breaking changes.

Restricting component access for different users

Different components support restricting access to them based on what operation types a user has permission for. This is based on the operation (CREATE, READ, UPDATE, DELETE) and the user’s scopes (PRODUCT, MENU, PRICE_LIST). Thus, these restrictions are primarily for action components. To check the user’s access, import the useHasAccess hook. It takes one or more scopes and one or more operation types

import { hooks } from '@broadleaf/admin-components';

const { useHasAccess } = hooks;

export const MyComponent = props => {
  const hasAccess = useHasAccess(
    props.metadata.scope,
    props.metadata.operationType
  );

  if (!hasAccess){
    return null;
  }

  return <button>{props.metadata.label}</button>;
};

Making API Requests

The RequestUtils are provided to enable making API requests against the backend Microservices. We’re using axios under-the-hood to make these requests, but the RequestUtils provides a level of abstraction over it to handle some additional configuration for the admin client. It exposes specific methods for get, post, patch, put, and del as well as a generic request. Each take request configuration (axios docs), context state for compiling template strings (/product/${id}), and the previous data to compare with (important for patch requests).

Setting up the request config

The Endpoint metadata is set up with a shape matching the request config. Therefore, when you go to make a request, you should pass in the relevant endpoint from the metadata. Let’s look at an example where we set up a "get" request.

import { findIn, getIn } from 'lodash';
import { utils } from '@broadleaf/admin-components';

const { RequestUtils } = utils;

export const makeRequest = async props => {
  const endpoints = getIn(props.metadata, 'endpoints', []);
  const readEndpoint = findIn(endpoints, { type: 'READ' });
  return await RequestUtils.get(
    { ...readEndpoint },
    props.contextParams
  );
}

Setting up the request context

Something else we should set up for the request are the contextParams seen in the previous example. These provide values for headers like the X-Context-Request header and for filling in template variables in the path such as id in /products/${id}. To set up the contextParams, we’ll need the main component’s (such as a product) metadata and form state. This will often be built a bit differently depending on the type of View we’re in as some may be trackable, while others are not.

Let’s look at what a useContextParams hook might look like that we can use to build the contextParams for our previous request.

import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { get, getIn } from 'lodash';
import { hooks, unstable } from '@broadleaf/admin-components';

const { useTracking, useTranslateMode } = hooks;
const { ContextRequest } = unstable;

const { ContextParameters } = ContextRequest;

export const useContextParams = (metadata, state) => {
  // if the metadata marks this as trackable, this will return an object with
  // the tracking info needed for the context request header using the catalog,
  // tenant, and sandbox contexts
  const tracking = useTracking(metadata);
  // on main entity form, the ID is in the URL unless this is a create, in that
  // case there is no ID yet
  const { id } = useParams();
  // this is important for sandbox entities for grouping together their changes
  const containerName = getIn(metadata, `attributes.changeContainer`);
  // let's us get the translate mode context and actions to see if we're in it
  const translateMode = useTranslateMode();

  return useMemo(() => {
    let contextParams = {};

    if (id) {
      contextParams.id = id;
    }

    if (isSandboxDiscriminated(metadata) && containerName) {
      contextParams[ContextParameters.CHANGE_CONTAINER] = buildChangeContainer(
        containerName,
        id
      );
    }

    if (tracking) {
      contextParams[ContextParameters.TRACKING] = tracking;
    }

    if (metadata.translatable && translateMode.isActive) {
      contextParams[ContextParameters.LOCALE] = translateMode.locale;
    }

    // this is important for compile the template paths such as
    // `/products/${parent.id}/variants/${id}`
    if (state && state.data) {
      contextParams.parent = state.data;

      if (state.data.contextState) {
        contextParams[ContextParameters.CONTEXT_STATE] =
          state.data.contextState;
      }
    }

    return contextParams;
  }, [state, tracking]);
}

const isSandboxDiscriminated = metadata => {
  return !!get(metadata, 'attributes.sandboxDiscriminated', false);
}

const buildChangeContainer = (containerName, containerId, subContainerName) => {
  const changeContainer = { name: containerName };

  if (!!containerId) {
    changeContainer.id = containerId;
  }

  if (!!subContainerName) {
    changeContainer.subContainerName = subContainerName;
  }

  return changeContainer;
}