Adding areas to the navigation menu

An area is an item in the navigation menu. You can add areas to the navigation menu, a useful feature if you want to make your custom widgets or plugins easily accessible to editors.

A role must have permission to access an area before that area appears in the role’s navigation menu.

However, access to an admin page (or any other content) is governed by the Page or Servlet that implements it; even if a role does not have access to an area, a user could paste the URL into the browser’s address bar and gain access to the area. To avoid this scenario, extend PageServlet as described in the snippet Checking permissions to view an area.

To add an area to the Navigation menu you must supply a com.psddev.cms.tool.Area object. An Area object contains four properties (among others):

The following sections explain these properties in detail.

See also:

displayName—an area’s label

displayName represents the label appearing in the navigation menu. The label can be a string literal, but you should consider adding hooks for localization to ensure the label can be displayed in other languages via resource bundles. The following snippet is an example of using Localization to display an area’s localized label at run time.

String displayName = Localization.currentUserText(ExampleServlet.class, "displayName", "Example Servlet");

The string Example Servlet serves as the fallback if there are no resource bundles matching the user’s preferred locale.

See also:

internalName—an area’s unique identifier

internalName represents an area’s identifier that must be unique over all areas and plugins. Consider using a namespace prefix for your identifiers to prevent conflicts. You can check if your desired internalName is already in use by running the following code in the /_debug/code tool.

public class Code {
    public static Object main() throws Throwable {
        return com.psddev.cms.tool.Tool.Static.getPlugins().stream()
            .map(p -> p.getInternalName())
            .collect(java.util.stream.Collectors.toList());
    }
}

Administrators can disable plugins and areas for all users by listing the corresponding internalName in the Disabled Plugins field found in Navigation menu > Admin > Sites & Settings > Global > Debug. For that reason, and as a best practice, consider using internalNames that provide a cue for the corresponding area, such as adminAbout, adminUsers, and adminThemes.

See also:

hierarchy—an area’s position in the navigation menu

hierarchy represents an area’s position in the navigation menu. The position can be at the top level (such as Admin) or at a second level (such as Admin > Sites & Settings). (Hierarchy below the second level is currently not supported.)

You use a slash / to indicate an area’s level in the hierarchy. For example, if you want to place an area with internalName of customImport under the Admin label, set hierarchy to admin/customImport.

At run time, Brightspot sorts the areas alphabetically level-by-level by their displayName with the following exceptions:

  • The top-level area dashboard/ and its children are always displayed first.
  • The top-level area admin/ and its children are always displayed last.

The value for hierarchy must be unique across all areas. You can check if your desired hierarchy is already in use by running the following code in the /_debug/code tool.

public class Code {
    public static Object main() throws Throwable {
        return com.psddev.cms.tool.Tool.Static.getPlugins()
            .stream()
            .filter(p -> p instanceof com.psddev.cms.tool.Area)
            .map(a -> (com.psddev.cms.tool.Area) a)
            .map(com.psddev.cms.tool.Area::getHierarchy)
            .sorted()
            .collect(Collectors.toList());
    }
}

See also:

Creating areas

There are two approaches for creating areas:

  • Overriding Tool#getPlugins. This approach is slightly more involved, but is best when building a reusable plugin that may be shared across several projects.
  • Implementing AutoArea. This approach is simpler, and is best when you have a one-off, project-specific area that is not part of a larger plugin.

The following sections provide detailed explanation for each approach.

Tool#getPlugins

This approach requires that you first have a class that extends Tool. In your subclass override the getPlugins method which expects a return value of type List<Plugin>. (Area is a subclass of Plugin, so you can include areas in the return value.) The following snippet adds a new area under the Admin section named Docs Example Area that provides a link to the Brightspot documentation site.

import java.util.ArrayList;
import java.util.List;

import com.psddev.cms.tool.Plugin;
import com.psddev.cms.tool.Tool;

public class ExampleTool extends Tool {

    @Override
    public List<Plugin> getPlugins() {
        List<Plugin> plugins = new ArrayList<>();

        Area exampleArea = new Area();
        exampleArea.setDisplayName("Docs Example Area");
        exampleArea.setInternalName("docsExampleArea");
        exampleArea.setHierarchy("admin/docsExampleArea");
        exampleArea.setUrl("https://docs.brightspot.com");

        plugins.add(exampleArea);

        return plugins;
    }
}

The example above can be rewritten with the convenience method Tool#createArea2(String, String, String, String) as follows, in which the four arguments correspond to the four fields set in the previous snippet. Using this convenience method may future-proof your code in case the process for Area instantiation changes.

import java.util.ArrayList;
import java.util.List;

import com.psddev.cms.tool.Plugin;
import com.psddev.cms.tool.Tool;

public class ExampleTool extends Tool {

    @Override
    public List<Plugin> getPlugins() {
        List<Plugin> plugins = new ArrayList<>();

        plugins.add(createArea2(
            "Docs Example Area", // displayName
            "docsExampleArea", // internalName
            "admin/docsExampleArea", // hierarchy
            "https://docs.brightspot.com")); // url

        return plugins;
    }
}

Brightspot resolves relative URLs relative to the Servlet Context and the Tool’s application path. Suppose you implement an area as follows:

  • Your application’s .war file is named my-app.war, and your Tool is also mapped to the path by the same application name.
  • Your Tool class implements the Tool#getApplicationName method to return my-tool.
  • The URL returned from AutoArea#getUrl is /my-servlet.

In this case, the URL Brightspot provides to the area is /my-app/my-tool/myservlet.

Alternatively, if your .war file is named ROOT.war, and your application name is the empty string (""), then the final link value is /my-servlet. For more information about configuring tools, contact your Brightspot representative.

See also:

AutoArea

This is the quickest way to introduce a new area and requires minimal interaction with other components and concepts. Create a class that implements the AutoArea interface. This interface specifies four methods corresponding to displayName, internalName, hierarchy, and url. The following snippet adds a new area under Admin named Docs Example Area that provides a link to the Brightspot documentation site.

import com.psddev.cms.tool.AutoArea;

public class ExampleAutoArea implements AutoArea {

    @Override
    public String getDisplayName() {
        return "Docs Example Area";
    }

    @Override
    public String getInternalName() {
        return "docsExampleArea";
    }

    @Override
    public String getHierarchy() {
        return "admin/docsExampleArea";
    }

    @Override
    public String getUrl() {
        return "https://docs.brightspot.com";
    }
}

Brightspot uses ClassFinder to find all concrete classes that implement the AutoArea interface, and uses reflection to instantiate them so that the interface methods can be called. Make sure there is a default constructor or an explicit no-argument constructor present on your class, otherwise your area will not appear in the UI.

In this approach, Brightspot resolves relative URLs relative to the Servlet Context only. Suppose you implement an area as follows:

  • Your application’s .war file is named my-app.war.
  • You are linking to a Servlet with the URL pattern /my-servlet.

In this case, the URL returned from AutoArea#getUrl is /my-servlet. Brightspot prepends the .war file’s name to provide the area with a URL /my-app/myservlet.

Adding areas: a complete example

Below is an example using the approach Tool#getPlugins that leverages all of the best practices discussed in this section to create a custom plugin, page, and area.

Checking permissions to view an area
package example;

import java.io.IOException;
import javax.servlet.ServletException;

import com.psddev.cms.tool.PageServlet;
import com.psddev.cms.tool.ToolPageContext;
import com.psddev.dari.util.RoutingFilter;

import static example.ExamplePage.PATH;
import static com.psddev.dari.html.Nodes.*;

@RoutingFilter.Path(application = "example-plugin", value = PATH)
public class ExamplePage extends PageServlet {

    static final String PATH = "/example-page.jsp";

    @Override
    protected String getPermissionId() {
        return "area/examplePlugin/examplePage";
    }

    @Override
    protected void doService(ToolPageContext page) throws IOException, ServletException {
        page.writeHeader();
        page.write(P.with("It works!"));
        page.writeFooter();
    }
}
Adding an area to navigation menu
package example;

import java.util.ArrayList;
import java.util.List;

import com.psddev.cms.db.Localization;
import com.psddev.cms.tool.Plugin;
import com.psddev.cms.tool.Tool;

public class ExampleTool extends Tool {

    @Override
    public String getApplicationName() {
        return "example-plugin";
    }

    @Override
    public List<Plugin> getPlugins() {
        List<Plugin> plugins = new ArrayList<>();

        // parent area
        plugins.add(createArea2(
            Localization.currentUserText(ExampleTool.class, "title", "Example Plugin"), // displayName
            "examplePlugin", // internalName
            "examplePlugin", // hierarchy
            null)); // url

        // child area
        plugins.add(createArea2(
            Localization.currentUserText(ExamplePage.class, "title", "Example Page"), // displayName
            "examplePage", // internalName
            "examplePlugin/examplePage", // hierarchy
            ExamplePage.PATH)); // url

        return plugins;
    }
}