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. The following sections describe the proprties included in an Area
object.
See also:
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
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 > Admin > Sites & Settings > Global > Debug. For that reason, and as a best practice, consider using internalName
s that provide a cue for the corresponding area, such as adminAbout
, adminUsers
, and adminThemes
.
See also:
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());
}
}
url
represents the URL the user’s browser loads when clicking on an area.
When a user clicks on an area that is a leaf in the hierarchy, Brightspot redirects the user’s browser to the corresponding URL. In contrast, when a user clicks on a parent area that has children, Brightspot opens the area to expose its children—regardless if the parent has a URL. To view the parent’s URL, users may right-click and attempt to open the URL in a new browser tab or window. To accommodate this scenario, assign to the parent a URL that is the same as one of its children. For example, right-clicking Admin can take the user to Sites & Settings.
Ensure that URLs pointing to a page in Brightspot do not include the domain, because the domain could change based on environment. For example, the URL for Users & Roles is /admin/users.jsp
. Brightspot resolves the full URL based on your application context.
For examples showing how Brightspot resolves an area’s entire URL, see 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 theTool#getApplicationName
method to returnmy-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
.
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.
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();
}
}
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;
}
}
Brightspot displays classes in almost every widget—the content edit form, the search panel, Edit Site, and many other places. You can conditionally show or hide a class or its instances depending depending on Brightspot’s state at run time by implementing the abstract method ClassDisplay#shouldHide
.
For example, when doing a federated search, editors can select from external content providers appearing in the External Types section of content type filter.
If your instance of Brightspot is not integrated with a particular service, you should prevent it from appearing in the External Types list by implementing ClassDisplay#shouldHide
. If this method returns true, Brightspot hides the class from the display; if it returns false, Brightspot shows the class in the display.
import com.psddev.cms.tool.ClassDisplay;
import com.psddev.dari.util.ObjectUtils;
public class VimeoClassDisplay implements ClassDisplay {
@Override
public boolean shouldHide(Class<?> instanceClass) {
/* Ensure the current class is a subclass of VimeoVideo */
if (VimeoVideo.class.isAssignableFrom(instanceClass)) {
VimeoSettings settings = VimeoUtils.getSettings();
/* Ensure the API key and secret are configured for the Vimeo service */
return ObjectUtils.isBlank(settings.getApiKey()) || ObjectUtils.isBlank(settings.getApiSecret());
}
return false;
}
}