In one of our projects we have few hundreds pages managed by about thirty editors. They have access rights to all pages across the system and we don’t want to limit them. The number of content items is large, but each editor is responsible for updating just few pages related with the branch where he works. In the end most of the time they are working with same pages. To locate the content they have to expand few levels of site tree or find them using search functionality. It’s quite a lot of clicks. That’s why we prepared content shortcuts functionality – the Editor Favourite Contents.
Editor Favourite Contents overview
There is a new sidebar button used to add/remove content to favourites and the gadget that displays already selected pages. The edit mode with “My favourite contents” is show below.
Sidebar button
The sidebar button is toggled – adding current content to favourite list or when content is already added, to remove content from favourite list.
Button use addToFavouritesCommand command. The _execute method switch favourite content state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
define([ "dojo/_base/declare", "dojo/_base/lang", "dojo/when", "epi/shell/command/_Command", "epi/dependency", "epi-cms/_ContentContextMixin", "./favouriteContextMixin" ], function( declare, lang, when, _Command, dependency, _ContentContextMixin, FavouriteContextMixin ) { return declare([_Command, _ContentContextMixin, FavouriteContextMixin], { name: "ContentReferences", label: "Favourite content", tooltip: "Favourite content", iconClass: 'epi-icon--medium epi-iconStar', canExecute: true, addedToFavourite: false, constructor: function() { var registry = dependency.resolve("epi.storeregistry"); this.favouriteContentStore = registry.get("alloy.favouriteContentStore"); var contentId = this.getCurrentContext().id; this.favouriteContentStore.get(contentId).then(lang.hitch(this, function (result) { this.favouriteContentChanged(contentId, result); //this._updateFavouriteStatus(result); })); }, _execute: function() { this.addedToFavourite = !this.addedToFavourite; when(this.getCurrentContext(), lang.hitch(this, function(currentContext) { var postParams = { contentReference: currentContext.id, addToFavourite: this.addedToFavourite }; this.favouriteContentStore.add(postParams).then(lang.hitch(this, function (result) { this.favouriteContentChanged(currentContext.id, result.addToFavourite); })); })); }, contentContextChanged: function(content) { this.favouriteContentStore.get(content.id).then(lang.hitch(this, function (result) { this._updateFavouriteStatus(result); })); }, _updateFavouriteStatus: function(result) { this.set("active", result); this.set('label', result ? "Remove from favourites" : "Add to your favourites"); this.addedToFavourite = result; }, onFavouriteContentRemoveAll: function() { this._updateFavouriteStatus(false); }, onFavouriteContentChanged: function (contentId, adToFavourite) { this._updateFavouriteStatus(adToFavourite); } }); }); |
The command is registired in commandsProvider.js.
1 2 3 4 5 6 7 |
this.addToLeading(addToFavouritesCommand, { showLabel: false, widget: ToggleButton, 'class': 'favourite-button' //'epi-disabledDropdownArrow epi-groupedButtonContainer'//'epi-leadingToggleButton epi-disabledDropdownArrow dijitDropDownButton' // dijitChecked }); |
My Favourite Content widget
To display the list of all favorite contents for currently logged editor we prepared edit mode gadget. It’s a grid with three columns – content name, publish state and special column with row context menu.
The widget shows all editor’s favourites, but also could be used to manage favourites. It has several functionalities, like:
- Add current content to favourites – works the same as toggle button in adding content state
- Remove all – used to remove all collected favourites
- Refresh – refresh the grid by fetching data from server
Each grid row has a context menu with two options:
- Edit – opens selected content. The content can be also opened by double click on the grid row
- Remove – removes selected content from favourites – works the same as toggle button in remove content state
The grid extends EPiServer _GridWidgetBase which is a base widget for Recently Changed component.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
define([ //... ], function ( //... ) { return declare([_GridWidgetBase, _WidgetCommandProviderMixin, _FocusableMixin, _ContentContextMixin, FavouriteContextMixin], { _componentsController: null, contextChangeEvent: "dblclick", menuProvider: new ComponentCommandsProvider(), constructor: function () { var registry = dependency.resolve("epi.storeregistry"); this.favouriteContentStore = registry.get("alloy.favouriteContentStore"); }, postMixInProperties: function () { this.storeKeyName = "alloy.favouriteContentStore"; this._componentsController = dependency.resolve("epi.shell.controller.Components"); this.ignoreVersionWhenComparingLinks = false; this.inherited(arguments); }, buildRendering: function () { //... }, startup: function () { //... }, fetchData: function () { //... }, contentContextChanged: function (content) { //... }, onFavouriteContentChanged: function (contentId, addToFavourite) { // ... } }); }); |
The full source code for gadget can be found here.
Solution architecture
Both grid and button use favouriteContextMixin. It’s an abstraction for publishing topics related with adding/removing content to favourites. This mixin is similar to _ContentContextMixin.
Grid commands are populated using componentCommandsProvider
We also had to create module.config where we defined component initializer and added additional CSS file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?xml version="1.0" encoding="utf-8" ?> <module> <clientResources> <add dependency="epi-cms.widgets.base" path="scripts/commandsInitializer.js" resourceType="Script" /> </clientResources> <clientModule initializer="favourites.commandsInitializer"> <moduleDependencies> <add dependency="CMS" type="RunAfter" /> </moduleDependencies> </clientModule> <dojo> <paths> <add name="favourites" path="scripts" /> </paths> </dojo> <clientResources> <add name="epi-cms.widgets.base" path="scripts/content/favourite.css" resourceType="Style"> </add> </clientResources> </module> |
Below is the structure of JavaScript files.
REST API
Communication between client and server is implemented with FavouriteContentStore store class. It’s a standard EPiServer REST store that inherits from RestControllerBase. On client side store is available as “alloy.favouriteContentStore“.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
[RestStore("favouriteContentStore")] public class FavouriteContentStore : RestControllerBase { private readonly IFavouriteContentStorage _favouriteContentStorage; private readonly IContentQueryProvider _queryProvider; private readonly IContentRepository _contentRepository; private readonly IContentStoreModelCreator _contentStoreModelCreator; public FavouriteContentStore(IContentRepository contentRepository, IContentStoreModelCreator contentStoreModelCreator, IContentQueryProvider queryProvider, IFavouriteContentStorage favouriteContentStorage ) { this._contentRepository = contentRepository; this._contentStoreModelCreator = contentStoreModelCreator; this._queryProvider = queryProvider; this._favouriteContentStorage = favouriteContentStorage; } [HttpGet] public RestResult Get(ContentReference id, string query, ContentReference referenceId, string[] typeIdentifiers, bool? allLanguages, IEnumerable<SortColumn> sortColumns, ItemRange range) { //... } [HttpPost] public RestResult RemoveAll() { //... } [HttpPost] public ActionResult Post(PostFavouriteContent postFavouriteContent) { //... } } public class PostFavouriteContent { public ContentReference ContentReference { get; set; } public bool AddToFavourite { get; set; } } |
Saving the data
There is also of course a persistent storage for favorite content. We used the Dynamic Data Store, but it could be any storage that implements IFavouriteContentStorage interface.
1 2 3 4 5 6 7 8 9 |
public interface IFavouriteContentStorage { bool Add(IPrincipal principal, ContentReference contentReference); bool Remove(IPrincipal principal, ContentReference contentReference); IEnumerable<ContentReference> GetByUser(IPrincipal principal); bool RemoveAll(IPrincipal principal); } |
The interface expose basic methods for managing favourite content:
- Add – add content to user favourites
- Remove – remove content from user favourites
- GetByUser – get list of user’s favourites
- RemoveAll – remove all user’s favourites
The storage is registired as singleton using new Episerver abstractions for StructureMap.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[InitializableModule] [ModuleDependency(typeof(EPiServer.Web.InitializationModule))] public class FavouriteContentInitializableModule: IConfigurableModule { public void ConfigureContainer(ServiceConfigurationContext context) { context.Services.AddSingleton<IFavouriteContentStorage, FavouriteContentStorage>(); } public void Initialize(InitializationEngine context) { } public void Uninitialize(InitializationEngine context) { } } |
Below I showed all solution layers – client side, API and storage.
And here is the preview of how Editor Favourite Contents works.
The full source code is available on Gist.