Hi, I’m Dave Woestenborghs. You might remember me from my previous Skrift article, multiple articles on 24 days in Umbraco, my packages, forum posts or various Umbraco events in Europe. In this article I will show you how to change core backoffice functionality without modifying a line of Umbraco core code.
It all started with a tweet
Adding custom action inside the #umbraco "Save and publish" dropdown. Yay or nay?
— Dirk (@netaddicts) March 15, 2017
Dirk De Grave asked on Twitter if it was possible to add a custom button to the content editing view in Umbraco. Me with my big mouth answered that I know how to do that. Almost immediately the people from Skrift trapped me into writing an article about it. So be careful what you put on Twitter... the "Skriftonians" are watching you.
So why did I know how to do this
At the time of this Twitter conversation I was developing my Nexu package [shamelessplug]If you haven’t tried it yet, you should definitely do[/shamelessplug]
For this package I needed to customize the delete dialog of content and media items, as well as intercepting the unpublish action of content. This was needed to present the content editor with a list of items that are linking to the current content or media item.
As I wanted to support multiple Umbraco versions I needed to find a way to change this core functionality without modifying core code
Luckily I remembered an excellent article by Matt Brailsford about Tweaking the backend : http://24days.in/umbraco-cms/2015/umbraco-7-back-office-tweaks/
Especially the part about the interceptors would fit my needs.
Changing the delete dialogs
So the first thing I did was copy the markup from the core delete dialog to my own view and added some extra markup needed for my package.
I also updated the ng-controller directive to use my own controller. So that brings me to the next point. I didn’t want to copy the logic of the controller as well, because this can change with every Umbraco release. So I needed a way to make my controller "inherit" the core controller for the delete dialog. Luckily angular supports this using the extend method.
So I added following code to my controller:
angular.extend(this, $controller('Umbraco.Editors.Content.DeleteController', { $scope: $scope }));
Because of this I could reuse the functions from the core controller and didn’t have to copy the functions used to handle the confirm and cancel button to my own controller.
So with this all in place I needed to intercept the calls to the existing delete dialog view and replace it with my own. So this is where the angular interceptor that Matt wrote about in his article comes into play.
So this is what my interceptor looks like for the content delete dialog:
angular.module('umbraco.services').config([
'$httpProvider',
function ($httpProvider) {
$httpProvider.interceptors.push(['$q','$injector', 'notificationsService', function ($q,$injector, notificationsService) {
return {
'request': function (request) {
// Redirect any requests to built in content delete to our custom delete
if (request.url.indexOf("views/content/delete.html") === 0) {
request.url = '/App_Plugins/Nexu/views/content-delete.html';
}
return request || $q.when(request);
}
};
}]);
}]);
And now when right clicking on a content item and and selecting delete my custom dialog is shown, with a list of items linking to the current item (when found).
After that I did almost the same thing for the media dialog and that problem was tackled as well.
Remember to add all your JavaScript files to the package.manifest otherwise these will not be loaded by the Umbraco backend.
So intercepting the delete dialogs was fairly easy.
Intercepting the unpublish action
This one was a lot more challenging to tackle. When you click the Unpublish button in Umbraco no views are shown. It calls an API controller and shows a notification if it was successful or not.
So what I needed was to intercept the API call, check if there are links pointing to the content item, and if so show the user a notification that he tried to unpublish something that is linked from other items. The notification will have a OK button to complete the unpublish and a cancel button.
Luckily with the interceptors you can intercept API calls as well.
The interceptor looks like this:
angular.module('umbraco.services').config([
'$httpProvider',
function ($httpProvider) {
$httpProvider.interceptors.push(['$q','$injector', 'notificationsService', function ($q,$injector, notificationsService) {
return {
'request': function (request) {
// regex to check if it's a unpublish api call and extract the content id
var unpublishUrlRegex = /^\/umbraco\/backoffice\/UmbracoApi\/Content\/PostUnPublish\?id=(\d*)$/i;
// check if unpublished api call is made
if(unpublishUrlRegex.test(request.url)) {
// get the id from the url
var id = unpublishUrlRegex.exec(request.url)[1];
// get nexuResource
var nexuService = $injector.get('Our.Umbraco.Nexu.Resource');
// create deferred request, this will "pause" the api call until we resolve it
var deferred = $q.defer();
// get incoming links
nexuService.getIncomingLinks(id)
.then(function(result) {
// if incoming links are found, intercept unpublish and show custom notification
if (result.data.length > 0) {
notificationsService.add({
// the path of our custom notification view
view: "/App_Plugins/Nexu/views/unpublish-confirmation.html",
// arguments object we want to pass to our custom notification
args: {
links: result.data,
deferredPromise: deferred,
originalRequest : request
}
});
} else {
// execute request as normal
deferred.resolve(request);
}
});
// return deferred promise
return deferred.promise;
}
return request || $q.when(request);
}
};
}]);
}]);
This code will intercept the API call to unpublish a document and defers the Angular promise. Basically this means we “pause” the API call until we say it can proceed. So what we do is check with a regex that is a call to the unpublish API and if so extract the id of the content item that is being unpublished. Once we have that id we call our own API to check if there are incoming links for the content item. If not we will resolve the deferred promise and the unpublish will happen.
If there are incoming links we will show our own custom notification that will warn the user that the item has incoming links. If the user clicks okay in that dialog we will resolve the deferred promise.
Have a look at the code for the view and controller of the custom notification to get the whole picture.
So back to the tweet question
So with this knowledge it’s also possible to add a button to the content edit view.
I have prepared a small example that will do this. Download this zip file and extract it into your App_Plugins folder of your Umbraco installation. After that go to a content item and you will see a extra button with the text “Click me”.
With great flexibility comes great responsibility
What we are basically doing is changing core functionality. You could compare it with changing the Umbraco source code and running your own build. On each upgrade of Umbraco the chance exists that you code will break because the things you are intercepting have changed. If you do this in your project you have control over this because you determine when you do the upgrade (unless you are on Umbraco Cloud).
But especially package developers using this should watch Umbraco upgrades closely. You can end up having to do different intercepts for different umbraco versions. So test your packages as soon as a new release is out.
Also Chief Unicorn Niels Hartvig put this on twitter:
But I can assure you, no kittens were harmed writing this article.
See you next time!