How to limit actions to specific doctypes

Hi,
I’m doing some custom actions where I select a specific content item in the backoffice and then I have a custom modal that pops out. All works well but I’d like to find a way to limit when this action is available. e.g.

if I right click a node and it’s not of type “FAQPage” and/or it’s parent isn’t “AboutUsPage” then don’t display the action button “Copy to Locations”

I started going down the route of

async isApplicable(): Promise<boolean> {
	if (this.args.entityType !== UMB_DOCUMENT_ENTITY_TYPE || !this.args.unique) return false;

	

	// Check if the parent's contentType alias is "locationParent"
	return true;
}

But that doesn’t let me check the selected doctype - from my understanding.

What you need is a custom condition. Each extension in the backoffice can have one or more conditions attached to it. And a menu item is also just an extension.

I made a condition for instance where I only want to show a workspace view if the entity is in the document workspace view (don’t want it in the media section for instance) and has a certain composition attached to it.

Or based on some custom logic, remove the delete button on certain nodes.

1 Like

Thanks @LuukPeters that was really useful. I’ve now got a custom condition setup - following the docs, but I still cant see how I can check the docType of the content item or find a way to check what the parent is of that node.
Do I need to make a request to a custom API endpoint to then do something to find the parent node etc or is there a way to do all that within typescript and the condition?

Because my condition is pretty small, I can share it with you. I think it does exactly what you want. The contentTypeAliases also contain the aliases of compositions and in my case that’s what I needed.

import {
	UmbConditionConfigBase,
	UmbConditionControllerArguments,
	UmbExtensionCondition
} from '@umbraco-cms/backoffice/extension-api';
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content';

/**
 * Configuration for the ContentExpirationCondition
 */
export type ContentExpirationConditionConfig = UmbConditionConfigBase<'Pn.Condition.ContentExpiration'> & {
	match?: string;
};

/** 
 * Represents an Umbraco condition that checks if the current content implements the content expiration composition 
 */
export class ContentExpirationCondition extends UmbConditionBase<ContentExpirationConditionConfig> implements UmbExtensionCondition {
	constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<ContentExpirationConditionConfig>) {
		super(host, args);

		this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, (instance) => {
			this.observe(instance.structure.contentTypeAliases, (aliases) => {

				if (aliases.includes('expiringContent')) {
					this.permitted = true;
				} else {
					this.permitted = false;
				}
			});
		});
	}
}

// Declare the Condition Configuration Type in the global UmbExtensionConditionConfigMap interface:
declare global {
	interface UmbExtensionConditionConfigMap {
		ContentExpirationConditionConfig: ContentExpirationConditionConfig;
	}
}

Thanks, this is getting me a bit closer. Just need to see if I can find a settings / option to find what parent it has.

From the top of my head, the parent unique can also be accessed by the content workspace context.

Thanks Luuk. Spent a fair bit of time yesterday and today trying this, even pointed Copilot at the source code to see if I could get it to work it out for me but everything is suggested just throws errors. e.g. instance.parentId , instance.data.uniqueParent and many many many different things :smiley:

If I find the answer I’ll update here for anyone else that might hit this issue.

After a day of failing, I feel I’m missing something from my knowledge cause this is seems to be really difficult to troubleshoot. Probably my lack of typescript skills doesnt help matters.

This is really just a reply to say, if anyone has any ideas how I can check to see what the parent of the currently selected node is and either allow or disallow my backoffice extension from displaying, I would be really grateful :slight_smile:

O>

I recommend using the umb-debug HTML element to help you debug.
Inspect with the browser devtools and amend the HTML to a DOM element close to where you want to know about available contexts.

<umb-debug visible dialog></umb-debug>

Then you can see a red badge that opens a dialog to help you see contexts and their properties or methods that you could consider using.

An image shows a computer screen with the UI of a content management system, the HTML code, and debug contexts, which are all marked with red arrows. (Captioned by AI)

I don’t know exactly what is in UmbParentEntityContext but its something worth looking at seeing if its of any use to you Owain.

So consume this context in your condition and let Typescript tooling to help you explore what the context gives you in either methods or properties.

Happy Hacking

Thanks @warren - didn’t know about that and it’s helpful however, I dont get UmbParentEntityContext available when I drop the debug in pretty much the same location as you have. Which is odd but doesn’t surprise me :slight_smile:

I’ll keep digging.

O.

Trying something along these lines just now

export class CopyToCondition extends UmbConditionBase<CopytoCondtionConfig> implements UmbExtensionCondition {
	constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<CopytoCondtionConfig>) {
		super(host, args);
		this.consumeContext(UMB_ANCESTORS_ENTITY_CONTEXT, (instance) => {
			this.observe(instance.ancestors, (ancestors) => {
				// Check is the anestor is a spefic document type or alias
				const hasLocationDetail = ancestors.some(
					(ancestor) => ancestor.entityType === 'LocationDetail'
				);
				this.permitted = hasLocationDetail;
			});
		});
		
		this.permitted = true;
	}
}

I know it’s not right as entityType isn’t the docType but maybe a possible way forward

But, I could be on the right route after all in a round about way - I can get the ancestors at least now, I can’t tell what it’s docType is yet but it’s progress.

More progress.

export class CopyToCondition extends UmbConditionBase<CopytoCondtionConfig> implements UmbExtensionCondition {
	constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<CopytoCondtionConfig>) {
		super(host, args);
		this.consumeContext(UMB_TREE_ITEM_CONTEXT, (instance) => {

			var treeItem = instance.getTreeItem();
			console.log(treeItem);
		});
		
		this.permitted = true;
	}
}

So I can now see the parent of the item and also the ancestors of that item. I only get the key for those so the next thing to see is, if there is some way to get details of a doctype by key.

It’s funny you are doing this but we have the foundation
Of what you re trying but rather than removing the action we want to give the user a message

Thanks @ravimotha - I had a look at the repo and it made me think of things in a different way. I now have it working. Not using Typescript but instead, like your repo, I made an API endpoint that allowed me to then check parents of the current node and also check ContentType Alias via c# and the content service.

The unedited version of my code:

typescript :

export class CopyToCondition extends UmbConditionBase<CopytoCondtionConfig> implements UmbExtensionCondition {
	constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<CopytoCondtionConfig>) {
		super(host, args);
		this.consumeContext(UMB_TREE_ITEM_CONTEXT, (instance) => {
			// Use an async IIFE to handle async logic
			(async () => {
				const treeItem = instance.getTreeItem();
				const currentId = treeItem?.unique;
				if (currentId) {
					try {
						const response = await TenpinCopyToService.getUmbracoManagementApiV1CopytoLocationsParentsById({
							path: { id: currentId } 
						});
						if (response.data == false) {
							this.permitted = false;
							return;
						}
						else {
							this.permitted = true;
						}
						
					} catch (error) {
						console.error('API error:', error);
					}
				}
				
			})();
		});
	}
}

Then the API endpoint in C#

[HttpGet("parents/{id}")]
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
[ProducesResponseType<bool>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetParents(string id)
{
	if (string.IsNullOrEmpty(id) || !Guid.TryParse(id, out var nodeId))
	{
		return BadRequest("Invalid node ID.");
	}
	var content = _contentService.GetById(nodeId);
	if (content == null)
	{
		return NotFound("Content not found.");
	}
	var parents = content.GetAncestorIds().ToList();

	foreach (var parentId in parents)
	{
		var parentContent = _contentService.GetById(parentId);
		if (parentContent != null  && parentContent.ContentType.Alias == LocationListingPage.ModelTypeAlias)
		{
			return Ok(true);
			
		}
	}
	return Ok(false);
	
}

I’ll check if I have time next week to look into this. Creating an endpoint really shouldn’t be needed.

1 Like

I’m not also doing some checks for some global settings too so it does a bit more now but would be interested to see if it’s possible but typescript only. :+1: