Customizing the UI with Actions¶
Review Board represents many pieces of the UI as “actions”, which are navigation links, buttons, or menu items that can be provided or customized by an extension.
These are used for:
Buttons, menus, and menu items in the review request action bar (used for closing a review request, uploading a file, archiving, and more)
Quick Access actions on the Review Banner.
The menus at the top of the page (account menu, Support, and Follow)
The administration UI sidebar items
If you want to add to any of those, you’ll need to define your own actions.
There are four key things to know about the action framework:
Actions are created by inheriting from
BaseAction(or a subclass), defining attributes that describe the action and its placements within the UI. Most actions provide a simple label that links to another page, but JavaScript can be used to provide more advanced behavior.Actions are placed in the UI using one or more
ActionPlacementinstances, which define the “attachment point” (the area of the page – useAttachmentPointfor a predefined list of options) and the ID of any parent action to place it within.Actions are rendered by a
BaseActionRenderer. Each placement or parent action may define the renderer used to render any actions placed there. This may render as a button, a menu, a menu item, or something else. Actions may also provide their own renderer, if they need to.Actions are registered by passing them to an
ActionHookwhen instantiating the extension.
Creating an Action¶
A basic action¶
To create an action, subclass
BaseAction and provide the following
attributes:
action_id: The ID of the action. This must be unique to your action.label: A user-visible label for the action.verbose_label: A more verbose user-visible label for the action.This is used for certain renders where space is available, and as a descriptive label for screen readers and other accessibility tools.
description: An accompanying description for your action.This will be a list of strings, each used as a paragraph.
Each string is plain text, which will be escaped for HTML. It should not contain HTML itself.
Descriptions will be used for button tooltips and as additional text for detailed menu items (such as those shown in the Review menu). Other renderers may represent them in other ways, or ignore them entirely.
placements: A list of places in the UI where the action should show up.This includes the “attachment” (the location within the UI) and an optional ID of the parent action to place it within.
Built-in attachment points can be found in
reviewboard.actions.base.AttachmentPoint. You can also define your own attachment points by setting this to a unique string and then using theactions_html()template tag.
A basic example would be:
from reviewboard.actions.base import (ActionPlacement,
AttachmentPoint,
BaseAction)
class MyAction(BaseAction):
action_id = 'my-action'
label = 'My Action'
verbose_label = 'My very special action'
placements = [
ActionPlacement(attachment=AttachmentPoint.HEADER),
]
This will place an action in the header area (where your account menu lives). But this action won’t do anything by itself.
Dynamic labels¶
Your action may want to show a different label based on the context of the page. For example, maybe the label should incorporate someone’s username or data from a database.
Instead of setting the attributes above, you can override these methods:
get_label(): Returns the label to use.By default, this just returns
label, but can be overridden to return a dynamic result:from django.template import Context ... class MyAction(BaseAction): ... def get_label( self, *, context: Context, ) -> str: user = context['request'].user if user.is_anonymous: return 'Public tasks' else: return f"{user.username}'s tasks"
get_verbose_label(): Returns the verbose label to use.By default, this just returns
verbose_label, but can be overridden to return a dynamic result:from django.template import Context ... class MyAction(BaseAction): ... def get_verbose_label( self, *, context: Context, ) -> str: user = context['request'].user if user.is_anonymous: return 'All public tasks' else: return f"All of {user.username}'s tasks"
Linking to a URL¶
Often, you’ll want to make your action link somewhere (unless you’re using JavaScript for managing the action – see below).
To define a URL for your action, use one of:
url: A URL to link to for the action. The default is'#', which enables handling the action via JavaScript.url_name: The name of a URL registered via URLHook. This is generally preferred over url and takes precedence.
You can also override the URL with a method:
get_url(): Returns the URL to use.By default this will just check which URL attribute was set and return the appropriate resolved URL.
from reviewboard.actions.base import (ActionPlacement,
AttachmentPoint,
BaseAction)
class MyAction(BaseAction):
action_id = 'my-action'
label = 'My Action'
# Either:
url = 'https://corp.example.com/my-tool/'
# Or:
url_name = 'my-registered-url-name'
placements = [
ActionPlacement(attachment=AttachmentPoint.HEADER),
]
Adding only to certain pages¶
If you want your extension to show up only to certain pages, use:
apply_to: A list of URL names where the action should appear (assuming the placement or parent is available on those pages).There are many pre-defined URL names that might be useful to you.
If this is not set (the default), this will appear on all pages that match any listed placements.
For example, to show up only on the dashboard and reviewable pages (review request page, diff viewer, and file attachments):
from reviewboard.actions.base import (ActionPlacement,
AttachmentPoint,
BaseAction)
from reviewboard.reviews.actions import all_review_request_url_names
class MyAction(BaseAction):
action_id = 'my-action'
label = 'My Action'
url_name = 'my-registered-url-name'
apply_to = [
'dashboard',
*all_review_request_url_names,
]
placements = [
ActionPlacement(attachment=AttachmentPoint.HEADER),
]
Controlling visibility¶
Sometimes you want your action to be shown only under certain circumstances. It might only apply to certain users or repository types.
The following attributes can control visibility:
visible: Whether the action should be visible by defaultIf set to
False, the action will still be rendered on the page, but will be hidden by default. JavaScript can then show the action on demand.class MyAction(BaseAction): visible = False
The following methods can further influence visibility:
get_visible(): Returns whether the action should be visible.This returns
visibleby default, but you can extend the action to base the result on data in the template context.For example:
from django.template import Context ... class MyAction(BaseAction): ... def get_visible( self, *, context: Context, ) -> bool: # Only show for anonymous users. return context['request'].user.is_anonymous
should_render(): Returns whether the action should even be rendered onto the page.This can be used to keep an action from being at all included on the page. Normally, this checks
apply_toand HideActionHook to determine the result.If you extend this, you should call the parent method first and return its result if
False.from django.template import Context ... class MyAction(BaseAction): ... def should_render( self, *, context: Context, ) -> bool: # Only show for Bob. return ( super().should_render(context=context) and context['request'].user.username == 'bob' )
Custom JavaScript models and state¶
Each action has a central JavaScript model that contains information about the action. Your action may want to provide more information that your JavaScript extension code can set or use.
The following attributes may be useful:
js_model_class: The class of the JavaScript model to instantiate for the action.This may be necessary if you want to provide central logic on your action model that other code can call.
class MyAction(BaseAction): js_model_class = 'MyExtension.MyAction'
The following methods may also be useful:
get_js_model_data(): Return JavaScript model data for your action.If you override this, make sure to call the parent method and return its results along with yours.
from django.template import Context from typelets.django.json import SerializableDjangoJSONDict ... class MyAction(BaseAction): ... def get_js_model_data( self, *, context: Context, ) -> SerializableDjangoJSONDict: model_data = super().get_js_model_data(context=context) model_data['myExtraState'] = 123 return model_data
If you’ve set a custom JavaScript model, you’ll need to define your
JavaScript class as a subclass of RB.Actions.Action.
The main method to override is activate(), which is called when the user
triggers the action (for example, when a button or menu item representing
the action is clicked).
class MyAction extends RB.Actions.Action {
async activate() {
// Handle the action here.
const myExtraState = this.get('myExtraState');
}
}
MyExtension.MyAction = MyAction;
import { spina } from '@beanbag/spina';
import { Actions } from 'reviewboard/common';
@spina
export class MyAction extends Actions.Action {
async activate() {
// Handle the action here.
const myExtraState = this.get('myExtraState');
}
}
Any attributes you pass back from
get_js_model_data() on the
Python side will be available as model attributes, accessible via
this.get('attributeName').
Controlling rendering¶
For more advanced actions, you may also want to control the rendering of your action and the JavaScript state behind it.
The following attributes may be useful:
default_renderer_cls: The defaultBaseActionRenderersubclass to use if no other renderer is available.This is rarely required, but may be needed if you’re defining your own attachment points without their own default renderer.
js_template_name: The name of the template file to use for rendering the JavaScript side of the action’s state model.You will almost never need to change this.
Registering an Action¶
In order for your action to appear in the UI, or for your JavaScript to interact with it, you will need to register it. This is done by passing one or more action instances to an ActionHook:
class MyExtension(Extension):
def initialize(self) -> None:
ActionHook(self, actions=[
MyAction1(),
MyAction2(),
])
These will be registered and available for rendering on pages.
Writing an Action Renderer¶
For more advanced actions, you may want to control exactly how the action is
rendered on the page. This is done by subclassing
BaseActionRenderer and setting
your action’s
default_renderer_cls to
reference it.
The following attributes can be set on a renderer:
template_name: The path to a Django template for rendering the action’s HTML.js_view_class: The JavaScript view class to use for the rendered action. Review Board provides several built-in options:'RB.Actions.ActionView': The default base view.'RB.Actions.ButtonActionView': A clickable button that callsactivate()on the model when clicked.'RB.Actions.MenuActionView': A dropdown menu.'RB.Actions.MenuItemActionView': An item inside a dropdown menu that callsactivate()on the model when clicked.
You can also override the following method:
get_extra_context(): Return additional context variables for the template.Make sure to call the parent method and include its results.
For example:
from django.http import HttpRequest
from django.template import Context
from reviewboard.actions.base import BaseAction
from reviewboard.actions.renderers import BaseActionRenderer
class MyActionRenderer(BaseActionRenderer):
template_name = 'myextension/my_action.html'
js_view_class = 'RB.Actions.ButtonActionView'
def get_extra_context(
self,
*,
request: HttpRequest,
context: Context,
) -> dict:
return dict(
super().get_extra_context(request=request, context=context),
my_extra_data='...',
)
class MyAction(BaseAction):
...
default_renderer_cls = MyActionRenderer
This would render using the template specified in your action, which
should extend actions/action_base.html and implement the
action_content block. For example:
{% extends "actions/action_base.html" %}
{% block action_content %}
<a {{action_attrs}} href="{{url}}" role="button"
aria-label="{{verbose_label}}">
{{label}}
{% if my_extra_data %}
<strong>({{my_extra_data}})</strong>
{% endif %}
</a>
{% endblock action_content %}
The following context variables are automatically available:
action: The action object. Useful for accessing custom attributes, or attributes likeaction.icon_class.action_attrs: Pre-built HTML attributes for the inner element, including the ID and visibility state. Insert these directly into your element’s opening tag.dom_element_id: The unique DOM element ID for the action.label: The label to show on the action.url: The URL to navigate to, if suitable for the action.verbose_label: A verbose label, suitable for wider elements and for ARIA labels for screen readers and other assistive technologies.visible: Whether the action should appear visible.Any variables returned by
get_extra_context().
Writing Actions For…¶
Page Headers¶
The header area at the top of every page contains a set of menus:
Your account menu (when logged in)
Support
Follow
You can add your own standalone menus or links alongside these, or add items to any of these built-in menus.
To place an action in the header, use a placement attaching to
HEADER.
Review Request Action Bar¶
The review request action bar contains the buttons and menus shown on the review request page, such as Update, Close, and Download Diff.
To place an action in the action bar:
Use a placement attaching to
REVIEW_REQUEST(for the actions on the right) orREVIEW_REQUEST_ELFT(for the actions on the left).Set
apply_totoreviewboard.reviews.actions.all_review_request_url_names.
from reviewboard.actions.base import (ActionPlacement,
AttachmentPoint,
BaseAction)
from reviewboard.reviews.actions import all_review_request_url_names
class MyReviewRequestAction(BaseAction):
action_id = 'my-review-request-action'
label = 'My Action'
apply_to = all_review_request_url_names
placements = [
ActionPlacement(attachment=AttachmentPoint.REVIEW_REQUEST),
]
You’ll probably want to define a custom JavaScript model to control what happens when this is clicked.