The right-click menu on every tree in the editor is extensible. An action you write appears on the menu beside Cut, Copy and Paste, on the kinds of item you choose, and is handed the item the author clicked.
An action is JavaScript: it runs in the editor, in the author's browser. Anything it needs from the server it asks for from an endpoint of yours, which checks the author's rights itself.
Step 1: write the script
Add a script to your site's wwwroot. This example is wwwroot/js/cms-tree-actions.js.
window.cmsExtensions.addTreeAction({
target: 'page',
label: 'Send for Translation',
visible: function (node, context) { return context.holds('Edit'); },
action: function (node, context) {
fetch('/api/translations/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemId: node.itemId, culture: context.culture })
}).then(function () { context.reload(); });
}
});
| Property | Required | What it does |
|---|---|---|
target |
yes | Which items carry the action. One of the targets below, or an array of them. |
label |
yes | What the menu says. A string, or a function (node, context) returning one. |
action |
yes | Runs when the author chooses it. Receives (node, context). |
icon |
no | One character shown before the label. |
visible |
no | A function (node, context). Return false to leave the action off this item's menu. |
enabled |
no | A function (node, context). Return false to show the action greyed out. |
order |
no | Position among extension actions; lower is higher. Ties keep the order they were added in. |
danger |
no | true draws the row in the destructive colour. |
Targets
| Target | The item |
|---|---|
page |
A page in the Pages tree |
block |
A block or a form |
blockFolder |
A folder in the Blocks or Forms tree |
media |
A media file |
mediaFolder |
A media folder |
category |
A category |
The node
node is the item as the tree holds it. Every target has an id and a name: node.itemId and
node.name (a category has node.categoryId). A page also carries urlSegment,
contentTypeId and origin; a media file carries category (Image, Document…).
The context
| Member | What it is |
|---|---|
context.target |
The target this menu was opened for. |
context.culture |
The language the author is working in. |
context.releaseSlug |
The release the author is working in, or null for Live. |
context.scope |
For blocks: shared, local or forms. |
context.holds(right) |
Whether the author holds Read, Edit, Create, Delete or Publish on this item. |
context.reload() |
Redraws the tree. Call it after an action that changed something. |
holds is for leaving an action off a menu where it would only be refused. It is not the check:
your endpoint decides.
Step 2: load it into the editor
Name the script in Program.cs:
builder.Services.AddCendia(options =>
{
builder.Configuration.GetSection("Cendia").Bind(options);
options.EditorScripts.Add("/js/cms-tree-actions.js");
});
The editor loads it after its own scripts and before the page starts. The address carries a hash of the file's content, so an edit is picked up without clearing a cache. The public site never requests it.
Step 3: run it
Restart the site, open the editor and right-click an item. Your actions are in a group of their own, above Move to Trash where the menu has one and at the end where it does not.
Rules to know
- One item at a time. With several blocks or files selected, the menu is the batch menu and extension actions are left off it.
- A label is text. A page name with
<in it is shown as written, not as markup. - An action that throws is reported in the browser console and loses only its own row. The rest of the menu still works.
- The menu costs nothing until it is opened. Registering an action makes no request.
visibleandenabledrun when an author right-clicks, so keep them to what the node and the context already say; do not call the server from them. - Use
window.cmsDialogfor a confirmation or a message (confirm,alert,prompt), so your action asks the way the rest of the CMS does.
From a package
A NuGet package does the same from its own AddXxx() call, with the script in the package's
wwwroot:
public static IServiceCollection AddAcmeTranslations(this IServiceCollection services) =>
services.AddCendiaExtension<AcmeTranslationsMarker>(extension => extension
.AddEditorScript("js/translations.js")
.AddEditorStylesheet("css/translations.css"));
A relative path is served from /_content/<assembly name>/, which is where a Razor Class
Library's wwwroot lands. If the package id differs from the assembly name, set
extension.AssetBasePath. A package's files load ahead of the site's own.