#18951: Adding bulk feature operations to Features screen.

Work Item: 18951

--HG--
branch : 1.x
This commit is contained in:
Sipke Schoorstra
2013-07-21 02:13:17 +02:00
parent 42e2eb630e
commit c56b0696c1
10 changed files with 232 additions and 112 deletions

View File

@@ -14,13 +14,14 @@ using Orchard.Modules.Events;
using Orchard.Modules.Models;
using Orchard.Modules.Services;
using Orchard.Modules.ViewModels;
using Orchard.Mvc;
using Orchard.Mvc.Extensions;
using Orchard.Recipes.Models;
using Orchard.Recipes.Services;
using Orchard.Reports.Services;
using Orchard.Security;
using Orchard.UI.Navigation;
using Orchard.UI.Notify;
using Orchard.Utility.Extensions;
namespace Orchard.Modules.Controllers {
public class AdminController : Controller {
@@ -178,46 +179,52 @@ namespace Orchard.Modules.Controllers {
return View(new FeaturesViewModel { Features = features });
}
[HttpPost]
public ActionResult Enable(string id, bool? force) {
[HttpPost, ActionName("Features")]
[FormValueRequired("submit.BulkExecute")]
public ActionResult FeaturesPOST(FeaturesBulkAction bulkAction, IList<string> featureIds, bool? force) {
if (!Services.Authorizer.Authorize(Permissions.ManageFeatures, T("Not allowed to manage features")))
return new HttpUnauthorizedResult();
if (string.IsNullOrEmpty(id))
return HttpNotFound();
if (featureIds == null || !featureIds.Any()) {
ModelState.AddModelError("featureIds", T("Please select one or more features."));
}
_moduleService.EnableFeatures(new[] { id }, force != null && (bool)force);
if (ModelState.IsValid) {
var availableFeatures = _moduleService.GetAvailableFeatures().ToList();
var selectedFeatures = availableFeatures.Where(x => featureIds.Contains(x.Descriptor.Id)).ToList();
var enabledFeatures = availableFeatures.Where(x => x.IsEnabled && featureIds.Contains(x.Descriptor.Id)).Select(x => x.Descriptor.Id).ToList();
var disabledFeatures = availableFeatures.Where(x => !x.IsEnabled && featureIds.Contains(x.Descriptor.Id)).Select(x => x.Descriptor.Id).ToList();
return RedirectToAction("Features");
}
[HttpPost]
public ActionResult Disable(string id, bool? force) {
if (!Services.Authorizer.Authorize(Permissions.ManageFeatures, T("Not allowed to manage features")))
return new HttpUnauthorizedResult();
if (string.IsNullOrEmpty(id))
return HttpNotFound();
_moduleService.DisableFeatures(new[] { id }, force != null && (bool)force);
return RedirectToAction("Features");
}
[HttpPost]
public ActionResult Update(string id) {
if (!Services.Authorizer.Authorize(Permissions.ManageFeatures, T("Not allowed to manage features")))
return new HttpUnauthorizedResult();
if (string.IsNullOrEmpty(id))
return HttpNotFound();
try {
_reportsCoordinator.Register("Data Migration", "Upgrade " + id, "Orchard installation");
_dataMigrationManager.Update(id);
Services.Notifier.Information(T("The feature {0} was updated successfully", id));
} catch (Exception exception) {
Services.Notifier.Error(T("An error occured while updating the feature {0}: {1}", id, exception.Message));
switch (bulkAction) {
case FeaturesBulkAction.None:
break;
case FeaturesBulkAction.Enable:
_moduleService.EnableFeatures(disabledFeatures, force == true);
break;
case FeaturesBulkAction.Disable:
_moduleService.DisableFeatures(enabledFeatures, force == true);
break;
case FeaturesBulkAction.Toggle:
_moduleService.EnableFeatures(disabledFeatures, force == true);
_moduleService.DisableFeatures(enabledFeatures, force == true);
break;
case FeaturesBulkAction.Update:
foreach (var feature in selectedFeatures.Where(x => x.NeedsUpdate)) {
var id = feature.Descriptor.Id;
try {
_reportsCoordinator.Register("Data Migration", "Upgrade " + id, "Orchard installation");
_dataMigrationManager.Update(id);
Services.Notifier.Information(T("The feature {0} was updated successfully", id));
}
catch (Exception exception) {
Services.Notifier.Error(T("An error occured while updating the feature {0}: {1}", id, exception.Message));
}
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
return RedirectToAction("Features");

View File

@@ -79,6 +79,9 @@
</ItemGroup>
<ItemGroup>
<Content Include="Module.txt" />
<Content Include="Scripts\features.admin.min.js">
<DependentUpon>features.admin.js</DependentUpon>
</Content>
<Content Include="Styles\images\menu.modules.png" />
<Content Include="Styles\images\new.gif" />
<Content Include="Styles\images\update.gif" />
@@ -117,6 +120,19 @@
<ItemGroup>
<Content Include="Views\Admin\Recipes.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Scripts\features.admin.js" />
</ItemGroup>
<ItemGroup>
<Content Include="Scripts\features.admin.min.js.map">
<DependentUpon>features.admin.js</DependentUpon>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="Scripts\Web.config">
<SubType>Designer</SubType>
</Content>
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.web>
<httpHandlers>
<!-- iis6 - for any request in this location, return via managed static file handler -->
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
</httpHandlers>
</system.web>
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
</staticContent>
<handlers accessPolicy="Script,Read">
<!--
iis7 - for any request to a file exists on disk, return it via native http module.
accessPolicy 'Script' is to allow for a managed 404 page.
-->
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
</handlers>
</system.webServer>
</configuration>

View File

@@ -0,0 +1,58 @@
$(function() {
var initializeFeaturesUI = function() {
var bulkActions = $(".bulk-actions-wrapper").addClass("visible");
var theSwitch = $(".switch-for-switchable");
theSwitch.prepend(bulkActions);
$("#search-box").focus().keyup(function() {
var text = $(this).val();
if (text == '') {
$("li.category").show();
$("li.feature:hidden").show();
return;
}
$("li.feature").each(function() {
var elt = $(this);
var value = elt.find('h3:first').text();
if (value.toLowerCase().indexOf(text.toLowerCase()) >= 0)
elt.show();
else
elt.hide();
});
$("li.category:hidden").show();
var toHide = $("li.category:not(:has(li.feature:visible))").hide();
});
};
var initializeSelectionBehavior = function() {
$("li.feature h3").on("change", "input[type='checkbox']", function() {
var checked = $(this).is(":checked");
var wrapper = $(this).parents("li.feature:first");
wrapper.toggleClass("selected", checked);
});
};
var initializeActionLinks = function() {
$("li.feature .actions").on("click", "a[data-feature-action]", function(e) {
var actionLink = $(this);
var featureId = actionLink.data("feature-id");
var action = actionLink.data("feature-action");
var force = actionLink.data("feature-force");
$("[name='submit.BulkExecute']").val("yes");
$("[name='featureIds']").val(featureId);
$("[name='bulkAction']").val(action);
$("[name='force']").val(force);
actionLink.parents("form:first").submit();
e.preventDefault();
});
};
initializeFeaturesUI();
initializeSelectionBehavior();
initializeActionLinks();
});

View File

@@ -0,0 +1,2 @@
$(function(){var n=function(){var n=$(".bulk-actions-wrapper").addClass("visible"),t=$(".switch-for-switchable");t.prepend(n),$("#search-box").focus().keyup(function(){var n=$(this).val(),t;if(n==""){$("li.category").show(),$("li.feature:hidden").show();return}$("li.feature").each(function(){var t=$(this),i=t.find("h3:first").text();i.toLowerCase().indexOf(n.toLowerCase())>=0?t.show():t.hide()}),$("li.category:hidden").show(),t=$("li.category:not(:has(li.feature:visible))").hide()})},t=function(){$("li.feature h3").on("change","input[type='checkbox']",function(){var n=$(this).is(":checked"),t=$(this).parents("li.feature:first");t.toggleClass("selected",n)})},i=function(){$("li.feature .actions").on("click","a[data-feature-action]",function(n){var t=$(this),i=t.data("feature-id"),r=t.data("feature-action"),u=t.data("feature-force");$("[name='submit.BulkExecute']").val("yes"),$("[name='featureIds']").val(i),$("[name='bulkAction']").val(r),$("[name='force']").val(u),t.parents("form:first").submit(),n.preventDefault()})};n(),t(),i()});
//@ sourceMappingURL=features.admin.min.js.map

View File

@@ -0,0 +1,8 @@
{
"version":3,
"file":"features.admin.min.js",
"lineCount":1,
"mappings":"AAAAA,CAAC,CAAC,QAAQ,CAAA,CAAG,CAET,IAAIC,EAAuBA,QAAQ,CAAA,CAAG,CAClC,IAAIC,EAAcF,CAAC,CAAC,uBAAD,CAAyBG,SAAS,CAAC,SAAD,EACjDC,EAAYJ,CAAC,CAAC,wBAAD,CAD+C,CAEhEI,CAASC,QAAQ,CAACH,CAAD,CAAa,CAC9BF,CAAC,CAAC,aAAD,CAAeM,MAAM,CAAA,CAAEC,MAAM,CAAC,QAAQ,CAAA,CAAG,CACtC,IAAIC,EAAOR,CAAC,CAAC,IAAD,CAAMS,IAAI,CAAA,EAkBlBC,CAlBoB,CAExB,GAAIF,CAAK,EAAG,GAAI,CACZR,CAAC,CAAC,aAAD,CAAeW,KAAK,CAAA,CAAE,CACvBX,CAAC,CAAC,mBAAD,CAAqBW,KAAK,CAAA,CAAE,CAC7B,MAHY,CAMhBX,CAAC,CAAC,YAAD,CAAcY,KAAK,CAAC,QAAQ,CAAA,CAAG,CAC5B,IAAIC,EAAMb,CAAC,CAAC,IAAD,EACPc,EAAQD,CAAGE,KAAK,CAAC,UAAD,CAAYP,KAAK,CAAA,CADpB,CAEbM,CAAKE,YAAY,CAAA,CAAEC,QAAQ,CAACT,CAAIQ,YAAY,CAAA,CAAjB,CAAqB,EAAG,CAAvD,CACIH,CAAGF,KAAK,CAAA,CADZ,CAGIE,CAAGK,KAAK,CAAA,CANgB,CAAZ,CAOlB,CAEFlB,CAAC,CAAC,oBAAD,CAAsBW,KAAK,CAAA,CAAE,CAC1BD,CAAO,CAAEV,CAAC,CAAC,2CAAD,CAA6CkB,KAAK,CAAA,CAnB1B,CAAZ,CAJI,EA2BlCC,EAA8BA,QAAQ,CAAA,CAAG,CACzCnB,CAAC,CAAC,eAAD,CAAiBoB,GAAG,CAAC,QAAQ,CAAE,wBAAwB,CAAE,QAAQ,CAAA,CAAG,CACjE,IAAIC,EAAUrB,CAAC,CAAC,IAAD,CAAMsB,GAAG,CAAC,UAAD,EACpBC,EAAUvB,CAAC,CAAC,IAAD,CAAMwB,QAAQ,CAAC,kBAAD,CADO,CAEpCD,CAAOE,YAAY,CAAC,UAAU,CAAEJ,CAAb,CAH8C,CAAhD,CADoB,EAQzCK,EAAwBA,QAAQ,CAAA,CAAG,CACnC1B,CAAC,CAAC,qBAAD,CAAuBoB,GAAG,CAAC,OAAO,CAAE,wBAAwB,CAAE,QAAQ,CAACO,CAAD,CAAI,CACvE,IAAIC,EAAa5B,CAAC,CAAC,IAAD,EACd6B,EAAYD,CAAUE,KAAK,CAAC,YAAD,EAC3BC,EAASH,CAAUE,KAAK,CAAC,gBAAD,EACxBE,EAAQJ,CAAUE,KAAK,CAAC,eAAD,CAHH,CAKxB9B,CAAC,CAAC,6BAAD,CAA+BS,IAAI,CAAC,KAAD,CAAO,CAC3CT,CAAC,CAAC,qBAAD,CAAuBS,IAAI,CAACoB,CAAD,CAAW,CACvC7B,CAAC,CAAC,qBAAD,CAAuBS,IAAI,CAACsB,CAAD,CAAQ,CACpC/B,CAAC,CAAC,gBAAD,CAAkBS,IAAI,CAACuB,CAAD,CAAO,CAE9BJ,CAAUJ,QAAQ,CAAC,YAAD,CAAcS,OAAO,CAAA,CAAE,CACzCN,CAACO,eAAe,CAAA,CAZuD,CAAhD,CADQ,CAVtC,CA2BDjC,CAAoB,CAAA,CAAE,CACtBkB,CAA2B,CAAA,CAAE,CAC7BO,CAAqB,CAAA,CAxDZ,CAAZ,CAyDC",
"sources":["features.admin.js"],
"names":["$","initializeFeaturesUI","bulkActions","addClass","theSwitch","prepend","focus","keyup","text","val","toHide","show","each","elt","value","find","toLowerCase","indexOf","hide","initializeSelectionBehavior","on","checked","is","wrapper","parents","toggleClass","initializeActionLinks","e","actionLink","featureId","data","action","force","submit","preventDefault"]
}

View File

@@ -4,5 +4,14 @@ using Orchard.Modules.Models;
namespace Orchard.Modules.ViewModels {
public class FeaturesViewModel {
public IEnumerable<ModuleFeature> Features { get; set; }
public FeaturesBulkAction BulkAction { get; set; }
}
public enum FeaturesBulkAction {
None,
Enable,
Disable,
Update,
Toggle
}
}

View File

@@ -9,25 +9,46 @@
Style.Require("ModulesAdmin");
Style.Require("Switchable");
Script.Require("Switchable");
Script.Include("features.admin.js", "features.admin.min.js").AtFoot();
Layout.Title = T("Modules").ToString();
}
@if (Model.Features.Count() > 0) {
<ul class="features summary-view switchable">@{
var featureGroups = Model.Features.OrderBy(f => f.Descriptor.Category, new DoghouseComparer("Core")).GroupBy(f => f.Descriptor.Category);
foreach (var featureGroup in featureGroups) {
var categoryName = LocalizedString.TextOrDefault(featureGroup.First().Descriptor.Category, T("Uncategorized"));
var categoryClassName = string.Format("category {0}", Html.Encode(categoryName.ToString().HtmlClassify()));
if (featureGroup == featureGroups.First()) {
categoryClassName += " first";
}
if (featureGroup == featureGroups.Last()) {
categoryClassName += " last";
}
@if (Model.Features.Any()) {
using (Html.BeginFormAntiForgeryPost()) {
@Html.Hidden("submit.BulkExecute")
@Html.Hidden("force", true)
@Html.Hidden("featureIds")
<div class="bulk-actions-wrapper">
<fieldset class="bulk-actions">
<label for="search-box">@T("Filter:")</label>
<input id="search-box" class="text-box" type="text" />
</fieldset>
<fieldset class="bulk-actions">
<label for="publishActions">@T("Actions:")</label>
<select id="publishActions" name="bulkAction">
@Html.SelectOption(Model.BulkAction, FeaturesBulkAction.None, T("Choose action...").ToString())
@Html.SelectOption(Model.BulkAction, FeaturesBulkAction.Enable, T("Enable").ToString())
@Html.SelectOption(Model.BulkAction, FeaturesBulkAction.Disable, T("Disable").ToString())
@Html.SelectOption(Model.BulkAction, FeaturesBulkAction.Toggle, T("Toggle").ToString())
</select>
<button type="submit" name="submit.BulkExecute" value="yes">@T("Execute")</button>
</fieldset>
</div>
<ul class="features summary-view switchable">@{
var featureGroups = Model.Features.OrderBy(f => f.Descriptor.Category, new DoghouseComparer("Core")).GroupBy(f => f.Descriptor.Category).ToList();
foreach (var featureGroup in featureGroups) {
var categoryName = LocalizedString.TextOrDefault(featureGroup.First().Descriptor.Category, T("Uncategorized"));
var categoryClassName = string.Format("category {0}", Html.Encode(categoryName.ToString().HtmlClassify()));
if (featureGroup == featureGroups.First()) {
categoryClassName += " first";
}
if (featureGroup == featureGroups.Last()) {
categoryClassName += " last";
}
bool showEnable, showDisable;
<li class="@categoryClassName">
<li class="@categoryClassName">
<h2>@categoryName</h2>
<ul>@{
var features = featureGroup.OrderBy(f => f.Descriptor.Name);
@@ -52,12 +73,17 @@
select (from f in Model.Features where f.Descriptor.Id.Equals(d, StringComparison.OrdinalIgnoreCase) select f).SingleOrDefault()).Where(f => f != null).OrderBy(f => f.Descriptor.Name);
var missingDependencies = feature.Descriptor.Dependencies
.Where(d => !Model.Features.Any(f => f.Descriptor.Id.Equals(d, StringComparison.OrdinalIgnoreCase)));
showDisable = categoryName.ToString() != "Core";
showEnable = !missingDependencies.Any() && feature.Descriptor.Id != "Orchard.Setup";
<li class="@featureClassName" id="@featureId" title="@T("{0} is {1}", Html.AttributeEncode(featureName), featureState)">
var showDisable = categoryName.ToString() != "Core";
var showEnable = !missingDependencies.Any() && feature.Descriptor.Id != "Orchard.Setup";
<li class="@featureClassName" id="@featureId" title="@T("{0} is {1}", Html.AttributeEncode(featureName), featureState)">
<div class="summary">
<div class="properties">
<h3>@featureName</h3>
<h3>
<label>
<input type="checkbox" name="featureIds" value="@feature.Descriptor.Id"/>
@featureName
</label>
</h3>
<p class="description" title="@feature.Descriptor.Description">@feature.Descriptor.Description</p>
@if (feature.Descriptor.Dependencies != null && feature.Descriptor.Dependencies.Any()) {
<div class="dependencies">
@@ -76,26 +102,15 @@
</div>
<div class="actions">
@if (showDisable && feature.IsEnabled) {
using (Html.BeginFormAntiForgeryPost(string.Format("{0}", Url.Action("Disable", new { area = "Orchard.Modules" })), FormMethod.Post, new {@class = "inline link"})) {
@Html.Hidden("id", feature.Descriptor.Id, new {id = ""})
@Html.Hidden("force", true)
<button type="submit">@T("Disable")</button>
}
<a href="#" data-feature-id="@feature.Descriptor.Id" data-feature-action="@FeaturesBulkAction.Disable" data-feature-force="true">@T("Disable")</a>
}
@if(showEnable && !feature.IsEnabled) {
using (Html.BeginFormAntiForgeryPost(string.Format("{0}", Url.Action("Enable", new { area = "Orchard.Modules" })), FormMethod.Post, new {@class = "inline link"})) {
@Html.Hidden("id", feature.Descriptor.Id, new { id = "" })
@Html.Hidden("force", true)
<button type="submit">@T("Enable")</button>
}
@if (showEnable && !feature.IsEnabled) {
<a href="#" data-feature-id="@feature.Descriptor.Id" data-feature-action="@FeaturesBulkAction.Enable" data-feature-force="true">@T("Enable")</a>
}
@if(feature.NeedsUpdate){
using (Html.BeginFormAntiForgeryPost(string.Format("{0}", Url.Action("Update", new { area = "Orchard.Modules" })), FormMethod.Post, new {@class = "inline link"})) {
@Html.Hidden("id", feature.Descriptor.Id, new { id = "" })
<button type="submit" class="update">@T("Update")</button>
}
@if (feature.NeedsUpdate) {
<a href="#" data-feature-id="@feature.Descriptor.Id" data-feature-action="@FeaturesBulkAction.Update" data-feature-force="false">@T("Update")</a>
}
</div>
</div>
@@ -103,36 +118,4 @@
}</ul>
</li>}
}</ul>}
@using(Script.Foot()) {
<script type="text/javascript">
//<![CDATA[
$(function () {
var searchBox = $("<fieldset class=\"bulk-actions\"><label for=\"search-box\">@T("Filter:")</label> <input id=\"search-box\" class=\"text-box\" type=\"text\" /></fieldset>");
var theSwitch = $(".switch-for-switchable");
theSwitch.prepend(searchBox);
$("#search-box").focus().keyup(function () {
var text = $(this).val();
if (text == '') {
$("li.category").show();
$("li.feature:hidden").show();
return;
}
$("li.feature").each(function () {
var elt = $(this);
var value = elt.find('h3:first').text();
if (value.toLowerCase().indexOf(text.toLowerCase()) >= 0)
elt.show();
else
elt.hide();
});
$("li.category:hidden").show();
var toHide = $("li.category:not(:has(li.feature:visible))").hide();
});
})
//]]>
</script>
}

View File

@@ -1,9 +1,6 @@
@model Orchard.Modules.ViewModels.ModulesIndexViewModel
@using Orchard.Localization;
@using Orchard.Modules.Models;
@using Orchard.Modules.Extensions;
@using Orchard.Mvc.Html;
@using Orchard.Utility.Extensions;
@{
Style.Require("ModulesAdmin");
@@ -23,7 +20,7 @@
<span>@Html.ActionLink(T("Install a module from your computer").ToString(), "AddModule", "PackagingServices", new { area = "Orchard.Packaging", returnUrl = HttpContext.Current.Request.RawUrl }, null)</span>
}
if (Model.Modules.Count() > 0) {
if (Model.Modules.Any()) {
<ul class="contentItems">
@foreach (ModuleEntry module in Model.Modules.OrderBy(m => m.Descriptor.Name)) {
<li>@Display.ModuleEntry(ContentPart: module)</li>

View File

@@ -36,6 +36,7 @@ html.dyn #main ul.features button { display:none; }
.features.detail-view .feature {
padding:.25em 0;
border-bottom:1px solid #CCC;
}
.features .enabled.feature {
background:#FFF;
@@ -44,6 +45,9 @@ html.dyn #main ul.features button { display:none; }
border-color:#cfe493;
overflow:hidden;
}
.features.summary-view .enabled.selected.feature, .features.summary-view .disabled.selected.feature {
border-color:rgb(60,130,46);
}
.features .disabled.feature {
background:#f3f3f3;
}
@@ -56,17 +60,22 @@ html.dyn #main ul.features button { display:none; }
.features.summary-view .update.feature {
border-color:#E77;
}
.features.detail-view .feature {
border-bottom:1px solid #CCC;
}
.features.detail-view .last.feature {
border:0;
}
.features .feature .summary {
overflow:hidden;
padding:.4em .5em;
position:relative;
overflow: hidden;
padding: .4em .5em;
position: relative;
cursor: default;
}
.features .feature .summary .properties h3 label {
padding: 0;
cursor: pointer;
}
.features .feature .summary .properties h3 label input {
vertical-align: 5%;
}
.features.detail-view .feature .summary {
overflow:visible;
@@ -154,4 +163,10 @@ h2.recentlyInstalledModule {padding:0 0 0 40px;}
.manage {
float: right;
}
.orchard-modules .bulk-actions-wrapper {
display: none;
}
.orchard-modules .bulk-actions-wrapper.visible {
display: inline;
}