Content Folders, which are located in assets pane gadget or multichannel content gadget, allow you to add any type of block or folders. But sometimes, for specific folders, we would like to store only one type of content and additional folders used for example for clustering or grouping. In this article, I will describe how to limit the types of content that can be added to a folder.
Example use case
For example, a client would like to have folders to store only one type of blocks. One folder will contain blocks with recipes and the other will contain blocks with recipe ingredients. In addition, some recipes and ingredients could be grouped in subfolders, such as “Swedish recipes”, “Spicy ingredients” etc. The structure would look as follows:
To force the Recipes folder to contain only recipe blocks and subfolders with recipy blocks we can add a new folder type that inherits from the built-in ContentFolder type. The inheriting folder will be defined with AvailableContentTypes attribute. Initial folders can be configured using InitializationModule. A similar example is described in the following article.
Below is a code with conte types definition
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 |
[SiteContentType(GUID = "30C5B6EA-D3CE-4EF9-A0B2-006CD4C87A08", GroupName = SystemTabNames.Content)] public class RecipeBlock : SiteBlockData { [Display(GroupName = SystemTabNames.Content)] [CultureSpecific] public virtual string RecipeName { get; set; } } [ContentType(GUID = "E5FB1D93-9AD4-4102-9201-A67AEBAF94D8")] [AvailableContentTypes(Availability.Specific, Include = new[] { typeof(RecipeBlock), typeof(RecipesContentFolder) })] public class RecipesContentFolder : ContentFolder { } [SiteContentType(GUID = "3BB0E9FF-5B00-4D28-BB49-F57822673AA1", GroupName = SystemTabNames.Content)] public class IngredientBlock : SiteBlockData { [Display(GroupName = SystemTabNames.Content)] public virtual string Unit { get; set; } } [ContentType(GUID = "8FBD6B7C-2A9F-46DE-9118-039400D264E3")] [AvailableContentTypes(Availability.Specific, Include = new[] { typeof(IngredientBlock), typeof(IngredientsContentFolder) })] public class IngredientsContentFolder : ContentFolder { } |
Adding custom folder type in Edit Mode
In Edit Mode, you can create different types of blocks and pages, while there is no built-in mechanism for creating a specific folder type. By default, only the base folder type ContentFolder can be created. This means that you can not create a folder “Swedish resipes”, which will be of type RecipeContentFolder.
This can be solved by registering custom command.
Multichannel component
To enable the creation of selected folder types in the Multichannel Content gadget, we can use “epi-cms/plugin-area/navigation-tree” plugin area. To add new folder, the code from default NewFolder command will be used, but it will be called with a different folder type.
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
define([ "dojo/_base/declare", "dojo/topic", "epi/_Module", "epi/dependency", "epi/shell/command/_Command", "epi/shell/selection", "epi-cms/plugin-area/navigation-tree", "epi-cms/asset/command/NewFolder", "epi-cms/widget/ContentForestStoreModel", "epi-cms/ApplicationSettings" ], function ( declare, topic, _Module, dependency, _Command, Selection, navigationTreePluginArea, NewFolderCommand, ContentForestStoreModel, ApplicationSettings ) { var Command = declare([_Command], { postscript: function (config) { this.selection = new Selection(); var store = dependency.resolve("epi.storeregistry").get("epi.cms.content.light"); var settings = { category: "context", model: new ContentForestStoreModel({ store: store, roots: [ApplicationSettings.rootPage] }), selection: this.selection, //clipboard: this.clipboardManager typeIdentifier: config.typeIdentifier }; this._newFolderCommand = new NewFolderCommand(settings); this.inherited(arguments); this.set("label", this._newFolderCommand.get("label")); this.set("iconClass", this._newFolderCommand.get("iconClass")); // a workaround to make sure that tree will be refreshed var self = this; var lastItem = null; topic.subscribe("/epi/cms/action/newfolder", function (item) { if (lastItem === item) { return; } lastItem = item; topic.publish("/epi/cms/upload", self.model.contentLink); setTimeout(function () { topic.publish("/epi/cms/action/newfolder", item); }, 200); }); }, _onModelChange: function () { if (!this.model || !this.model.capabilities || !this.model.capabilities.isContentFolder) { this.set("isAvailable", false); return; } this.selection.set("data", [{ type: "epi.cms.contentdata", data: this.model }]); // custom folders are not base identifiers and command is not available if (this._newFolderCommand.get("canExecute")) { this._newFolderCommand.set("isAvailable", true); } this.inherited(arguments); this.set("canExecute", this._newFolderCommand.get("canExecute")); this.set("isAvailable", this._newFolderCommand.get("isAvailable")); }, _execute: function () { this._newFolderCommand.execute(); } }); return declare([_Module], { initialize: function () { this.inherited(arguments); navigationTreePluginArea.add(new Command({ typeIdentifier: "alloytemplates.models.pages.recipescontentfolder" })); navigationTreePluginArea.add(new Command({ typeIdentifier: "alloytemplates.models.pages.ingredientscontentfolder" })); } }); }); |
In addition to the client code, we need to translate the texts used as the command label and the default name of the new folder.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?xml version="1.0" encoding="utf-8"?> <languages> <language name="English" id="en"> <contenttypes> ... <recipescontentfolder> <create>New Recipe folder</create> <newitemdefaultname>New Recipe folder</newitemdefaultname> </recipescontentfolder> <ingredientscontentfolder> <create>New Ingredient folder</create> <newitemdefaultname>New Ingredient Folder</newitemdefaultname> </ingredientscontentfolder> ... </contenttypes> </language> </languages> |
Alternative solution
If you don’t want to add code in the dojo, you can, for example, add a plugin in Admin Mode that allows you to select the parent folder and the type of folder to be added. However, this solution requires administrative rights.
1 2 3 4 5 6 |
ContentReference parentFolder = ... // parent folder link for example from input field string folderName = ... // folder name from input var newRecipeFolder = contentRepository.GetDefault<RecipesContentFolder>(parentFolder); newRecipeFolder.Name = folderName; contentRepository.Save(newRecipeFolder, SaveAction.Publish, AccessLevel.NoAccess); |
New custom folder type in assets pane gadget
To allow adding specific folders in the assets pane, we need to use “epi-cms/plugin-area/assets-pane” plugin area. As in the previous code snippet, we will use NewFolder command, but run with a different content type.
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 74 |
define([ "dojo/_base/declare", "dojo/topic", "epi/_Module", "epi/dependency", "epi/shell/command/_Command", "epi/shell/selection", "epi-cms/plugin-area/assets-pane", "epi-cms/asset/command/NewFolder", "epi-cms/widget/ContentForestStoreModel", "epi-cms/ApplicationSettings" ], function ( declare, topic, _Module, dependency, _Command, Selection, assetsPanePluginArea, NewFolderCommand, ContentForestStoreModel, ApplicationSettings ) { var Command = declare([NewFolderCommand], { postscript: function (config) { this.selection = new Selection(); var store = dependency.resolve("epi.storeregistry").get("epi.cms.content.light"); this.category = "context", this._contentForestStoreModel = new ContentForestStoreModel({ store: store, roots: [ApplicationSettings.rootPage] }); //clipboard: this.clipboardManager this.typeIdentifier = config.typeIdentifier; this.inherited(arguments); // a workaround to make sure that tree will be refreshed topic.subscribe("/epi/cms/action/newfolder", function (item) { window.location.reload(); }); }, _onModelChange: function () { var model = this.model[0]; this.model = this._contentForestStoreModel; if (!model || !model.capabilities || !model.capabilities.isContentFolder) { this.set("isAvailable", false); return; } this._contentModel = model; this.inherited(arguments); // custom folders are not base identifiers and command is not available this.set("isAvailable", true); }, _getSingleSelectionData: function () { return this._contentModel; } }); return declare([_Module], { initialize: function () { this.inherited(arguments); assetsPanePluginArea.add(new Command({ typeIdentifier: "alloytemplates.models.pages.recipescontentfolder" })); } }); }); |
By using folders with defined AvailableContentTypes, folders will only contain the specified block types