diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Modules/Controllers/AdminController.cs index 01a627808..2dab3b8de 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Controllers/AdminController.cs +++ b/src/Orchard.Web/Modules/Orchard.Modules/Controllers/AdminController.cs @@ -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 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"); diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Orchard.Modules.csproj b/src/Orchard.Web/Modules/Orchard.Modules/Orchard.Modules.csproj index 8870475e2..5b3da20cb 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Orchard.Modules.csproj +++ b/src/Orchard.Web/Modules/Orchard.Modules/Orchard.Modules.csproj @@ -79,6 +79,9 @@ + + features.admin.js + @@ -117,6 +120,19 @@ + + + + + + features.admin.js + + + + + Designer + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Scripts/Web.config b/src/Orchard.Web/Modules/Orchard.Modules/Scripts/Web.config new file mode 100644 index 000000000..56fa41e3e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Modules/Scripts/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Scripts/features.admin.js b/src/Orchard.Web/Modules/Orchard.Modules/Scripts/features.admin.js new file mode 100644 index 000000000..332484393 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Modules/Scripts/features.admin.js @@ -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(); +}); \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Scripts/features.admin.min.js b/src/Orchard.Web/Modules/Orchard.Modules/Scripts/features.admin.min.js new file mode 100644 index 000000000..290669a05 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Modules/Scripts/features.admin.min.js @@ -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 \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Scripts/features.admin.min.js.map b/src/Orchard.Web/Modules/Orchard.Modules/Scripts/features.admin.min.js.map new file mode 100644 index 000000000..bf48f7e9b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Modules/Scripts/features.admin.min.js.map @@ -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"] +} diff --git a/src/Orchard.Web/Modules/Orchard.Modules/ViewModels/FeaturesViewModel.cs b/src/Orchard.Web/Modules/Orchard.Modules/ViewModels/FeaturesViewModel.cs index b6ba84036..2ea35614f 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/ViewModels/FeaturesViewModel.cs +++ b/src/Orchard.Web/Modules/Orchard.Modules/ViewModels/FeaturesViewModel.cs @@ -4,5 +4,14 @@ using Orchard.Modules.Models; namespace Orchard.Modules.ViewModels { public class FeaturesViewModel { public IEnumerable Features { get; set; } + public FeaturesBulkAction BulkAction { get; set; } + } + + public enum FeaturesBulkAction { + None, + Enable, + Disable, + Update, + Toggle } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Views/Admin/Features.cshtml b/src/Orchard.Web/Modules/Orchard.Modules/Views/Admin/Features.cshtml index 6278d512e..0a05e1a43 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Views/Admin/Features.cshtml +++ b/src/Orchard.Web/Modules/Orchard.Modules/Views/Admin/Features.cshtml @@ -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) { -
    @{ - 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") +
    +
    + + +
    +
    + + + +
    +
    + +
      @{ + 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; -
    • +
    • @categoryName

        @{ 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"; -
      • + var showDisable = categoryName.ToString() != "Core"; + var showEnable = !missingDependencies.Any() && feature.Descriptor.Id != "Orchard.Setup"; +
      • -

        @featureName

        +

        + +

        @feature.Descriptor.Description

        @if (feature.Descriptor.Dependencies != null && feature.Descriptor.Dependencies.Any()) {
        @@ -76,26 +102,15 @@
        @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) - - } + @T("Disable") } - @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) - - } + @if (showEnable && !feature.IsEnabled) { + @T("Enable") } - @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 = "" }) - - } + @if (feature.NeedsUpdate) { + @T("Update") }
        @@ -103,36 +118,4 @@ }
    • } }
    } - -@using(Script.Foot()) { - } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Modules/Views/Admin/Index.cshtml b/src/Orchard.Web/Modules/Orchard.Modules/Views/Admin/Index.cshtml index f7e19d1c7..2400a31b1 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/Views/Admin/Index.cshtml +++ b/src/Orchard.Web/Modules/Orchard.Modules/Views/Admin/Index.cshtml @@ -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 @@ @Html.ActionLink(T("Install a module from your computer").ToString(), "AddModule", "PackagingServices", new { area = "Orchard.Packaging", returnUrl = HttpContext.Current.Request.RawUrl }, null) } - if (Model.Modules.Count() > 0) { + if (Model.Modules.Any()) {
      @foreach (ModuleEntry module in Model.Modules.OrderBy(m => m.Descriptor.Name)) {
    • @Display.ModuleEntry(ContentPart: module)
    • diff --git a/src/Orchard.Web/Modules/Orchard.Modules/styles/orchard-modules-admin.css b/src/Orchard.Web/Modules/Orchard.Modules/styles/orchard-modules-admin.css index 88eb3fcd2..0a41a8b9b 100644 --- a/src/Orchard.Web/Modules/Orchard.Modules/styles/orchard-modules-admin.css +++ b/src/Orchard.Web/Modules/Orchard.Modules/styles/orchard-modules-admin.css @@ -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; } \ No newline at end of file