There are many situations when the solution works on Development and Test environments, but there are some problems with Production. Few times I had to create test page, check how system behaves and then delete the page. There is no Permanent Delete option for single content, so I had to empty whole trash or leave the test page. That’s why I tried to prepare functionality of deleting single file from trash.
Replacing Trash view
Trash view is represented by “epi-cms/component/Trash” component. Widget class is returned by CmsContentContextResolver service. It’s not a configurable option, so I had to change the implementation of service. Registering new implementation for IUriContextResolver interface is quite difficult, because CmsContentContextResolver has internal constructor and all methods are non-virtual, so inheritance won’t work. Moreover, there is a second implementation – ProjectContextResolver so I had to change exactly CmsContentContextResolver instance.
Fortunately, developer can intercept existing services from EPiServer 9.10. Whenever ServiceLocator will ask for CmsContentContextResolver instance I will return custom implementation with extended trash view.
In AssignCustomTrashComponent method I replacing “epi-cms/component/Trash” with “customTrash/Trash”.
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 |
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))] public class UriContextResolverInitializer : IConfigurableModule { public void ConfigureContainer(ServiceConfigurationContext context) { context.Services .Intercept<IUriContextResolver>( (locator, uriContextResolver) => uriContextResolver is CmsContentContextResolver ? new CustomUriContextResolver((CmsContentContextResolver) uriContextResolver) : uriContextResolver) .Intercept<IUrlContextResolver>( (locator, urlContextResolver) => urlContextResolver is CmsContentContextResolver ? new CustomUriContextResolver((CmsContentContextResolver) urlContextResolver) : urlContextResolver); } public void Initialize(InitializationEngine context) { } public void Uninitialize(InitializationEngine context) { } } public class CustomUriContextResolver : IUriContextResolver, IUrlContextResolver { private readonly CmsContentContextResolver _cmsContentContextResolver; public string Name => this._cmsContentContextResolver.Name; public int SortOrder => this._cmsContentContextResolver.SortOrder; public CustomUriContextResolver(CmsContentContextResolver cmsContentContextResolver) { _cmsContentContextResolver = cmsContentContextResolver; } public bool TryResolveUri(Uri uri, out ClientContextBase instance) { var result = this._cmsContentContextResolver.TryResolveUri(uri, out instance); AssignCustomTrashComponent(instance); return result; } public bool TryResolveUrl(Uri url, out ClientContextBase instance) { var result = this._cmsContentContextResolver.TryResolveUrl(url, out instance); AssignCustomTrashComponent(instance); return result; } private static void AssignCustomTrashComponent(ClientContextBase instance) { if (instance != null) { var contentDataContext = (ContentDataContext)instance; if (contentDataContext.CustomViewType == "epi-cms/component/Trash") { contentDataContext.CustomViewType = "customTrash/Trash"; } } } } |
Implementing REST controller
When restoring content or emptying trash, the WasteBasketStore API controller methods are executed. I had to create new controller with PermanentDelete method.
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 |
[RestStore("extendedWastebasket")] public class ExtendedWasteBasketStore : RestControllerBase { private readonly IContentLoader _contentLoader; private readonly IContentRepository _contentRepository; private readonly IContentProviderManager _contentProviderManager; public ExtendedWasteBasketStore(IContentProviderManager contentProviderManager, IContentLoader contentLoader, IContentRepository contentRepository) { _contentProviderManager = contentProviderManager; _contentLoader = contentLoader; _contentRepository = contentRepository; } public RestResult PermanentDelete(ContentReference id) { Validator.ThrowIfNull("id", id); var content = this._contentLoader.Get<IContent>(id); if (!this._contentProviderManager.IsWastebasket(content.ParentLink)) { throw new HttpException(400, "Not a valid wastebasket."); } if (!content.QueryDistinctAccess(AccessLevel.Delete)) { throw new HttpException(403, "You do not have access rights to empty this waste basket."); } var actionResponse = new ActionResponse<ContentReference> {ExtraInformation = content.ParentLink }; this._contentRepository.Delete(id, true, AccessLevel.NoAccess); return this.Rest(actionResponse); } } |
The REST store has to be registered on client side using module initializer.
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 |
define([ "dojo", "dojo/_base/declare", "epi/_Module", "epi/dependency", "epi/routes", "epi/shell/store/JsonRest" ], function ( dojo, declare, _Module, dependency, routes, JsonRest ) { return declare([_Module], { initialize: function () { this.inherited(arguments); var registry = this.resolveDependency("epi.storeregistry"); //Register store registry.add("alloy.extendedWastebasket", new JsonRest({ target: this._getRestPath("extendedWastebasket"), idProperty: "contentLink" }) ); }, _getRestPath: function (name) { return routes.getRestPath({ moduleArea: "App", storeName: name }); } }); }); |
And then module initializer has to be set in module.config.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="utf-8" ?> <module> <clientResources> <add dependency="epi-cms.widgets.base" path="scripts/storeInitializer.js" resourceType="Script" /> </clientResources> <clientModule initializer="customTrash.storeInitializer"> <moduleDependencies> <add dependency="CMS" type="RunAfter" /> </moduleDependencies> </clientModule> <dojo> <paths> <add name="customTrash" path="scripts" /> </paths> </dojo> </module> |
Extending trash view
The trash component is a standard context based view. It has updateView method executed when the context has changed. View contains one main widget “epi-cms/widget/Trash”. To replace it with extended version I changed the templateString property and set “customTrash/TrashWidget” for data-dojo-type attribute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
define([ "dojo/_base/declare", "customTrash/TrashWidget", "epi-cms/component/Trash" ], function( declare, TrashWidget, Trash ) { return declare([Trash], { templateString: "<div class=\"epi-trashComponent\"><div data-dojo-type='customTrash/TrashWidget' data-dojo-attach-point='trash'></div></div>" }); }); |
In extended trash widget I added “Delete permanently” link. Link has to be created for each data grid row. It will be displayed next to Restore link. To add the link I replaced formatter for action column (trashItemListWidget._grid.columns.action.formatter). It’s a hack, but it was the easiest way that I found.
Then I assigned click event using “.epi-gridDeleteAction:click” selector. In click event the PermanentDelete method of ExtendedWastebasketStore is executed, and trash is updated.
Before deleting item, the Confirmation dialog is displayed. Window was implemented using “epi/shell/widget/dialog/Confirmation”.
Below is full widget source code.
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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
define([ "dojo/_base/declare", "dojo/_base/lang", "dojo/_base/array", "dojo/_base/Deferred", "dojo/topic", "dijit/registry", "epi", "epi/dependency", "epi-cms/widget/Trash", "epi/shell/widget/dialog/Confirmation", // Resources "epi/i18n!epi/cms/nls/episerver.cms.components.trash", "xstyle/css!./styles.css" ], function( declare, lang, array, Deferred, topic, registry, epi, dependency, Trash, Confirmation, resources ) { return declare([Trash], { _setQueryOptionsAttr: function() { this.inherited(arguments); var selectedTab = this.tabContainer.selectedChildWidget; if (!selectedTab || !selectedTab.itemListId) { return; } var trashItemListWidget = registry.byId(selectedTab.itemListId); if (trashItemListWidget._isDeleteActionModified) { return; } this._modifyActionColumn(trashItemListWidget); var storeRegistry = dependency.resolve("epi.storeregistry"); this._extendedWastebasketStore = storeRegistry.get("alloy.extendedWastebasket"); this._initDeleteOnClick(trashItemListWidget); trashItemListWidget._isDeleteActionModified = true; }, _craeteConfirmationDialog: function (row) { return new Confirmation({ title: "Delete", description: lang.replace("Are you sure you want to permanently delete '{name}'", row.data), onShow: lang.hitch(this, function() { var trashItemList = this.tabContainer.selectedChildWidget.getChildren()[0]; trashItemList.clearSelection(); }) }); }, _modifyActionColumn: function(trashItemListWidget) { var actionFormatterFunc = trashItemListWidget._grid.columns.action.formatter; function renderActionMenu(value) { return actionFormatterFunc() + "<a class=\"epi-gridDeleteAction epi-visibleLink\">Delete permanently</a>"; } trashItemListWidget._grid.columns.action.formatter = renderActionMenu; }, _initDeleteOnClick: function (trashItemListWidget) { trashItemListWidget._grid.on(".epi-gridDeleteAction:click", lang.hitch(this, function (evt) { var row = trashItemListWidget._grid.row(evt); var currentTrash = this.model.get("currentTrash"); if (!row.data || !currentTrash) { return; } var dialog = this._craeteConfirmationDialog(row); dialog.connect(dialog, "onAction", lang.hitch(this, function (confirm) { if (!confirm) { return; } var contentId = row.data.contentLink; Deferred.when(this._extendedWastebasketStore.executeMethod("PermanentDelete", contentId), lang.hitch(this, function (response) { var trashes = this.model.get("trashes"); array.forEach(trashes, lang.hitch(this, function (trash) { if (response.extraInformation === trash.wasteBasketLink) { trash.deletedByUsers = []; trash.isRequireLoad = true; trash.deletedByUsers = []; this.model.set("currentTrash", trash); } })); }), lang.hitch(this, function (response) { if (response.status === 403) { this.model.set("actionResponse", resources.emptytrash.accessdenied); } } )); })); dialog.show(); } )); } }); }); |
The source code is available on Gist