Broadleaf Microservices
  • v1.0.0-latest-prod

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.

Tip
Read more about the @broadleaf/admin-components component library and its exports here.

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;
}