diff --git a/OrchardCore.sln b/OrchardCore.sln index 65916d9971b..dd740f4577f 100644 --- a/OrchardCore.sln +++ b/OrchardCore.sln @@ -470,6 +470,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.FileStorage.Ama EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Media.AmazonS3", "src\OrchardCore.Modules\OrchardCore.Media.AmazonS3\OrchardCore.Media.AmazonS3.csproj", "{FF1C550C-6D30-499A-AF11-68DE7C8B6869}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.ArchiveLater", "src\OrchardCore.Modules\OrchardCore.ArchiveLater\OrchardCore.ArchiveLater.csproj", "{190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1242,6 +1244,10 @@ Global {FF1C550C-6D30-499A-AF11-68DE7C8B6869}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF1C550C-6D30-499A-AF11-68DE7C8B6869}.Release|Any CPU.ActiveCfg = Release|Any CPU {FF1C550C-6D30-499A-AF11-68DE7C8B6869}.Release|Any CPU.Build.0 = Release|Any CPU + {190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {190C4BEB-C506-4F7F-BDCA-93F3C1C221BC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1459,6 +1465,7 @@ Global {25C50837-551F-433C-9CED-DD0732523D3D} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} {38F43FA0-5BA8-4D6B-8F66-C708D590EF76} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} {FF1C550C-6D30-499A-AF11-68DE7C8B6869} = {90030E85-0C4F-456F-B879-443E8A3F220D} + {190C4BEB-C506-4F7F-BDCA-93F3C1C221BC} = {90030E85-0C4F-456F-B879-443E8A3F220D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46A1D25A-78D1-4476-9CBF-25B75E296341} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Assets.json b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Assets.json new file mode 100644 index 00000000000..de2ca1bdd49 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Assets.json @@ -0,0 +1,9 @@ +[ + { + "generateSourceMaps": false, + "inputs": [ + "Assets/scss/archive-later.scss" + ], + "output": "wwwroot/Styles/archive-later.css" + } +] diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Assets/scss/archive-later.scss b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Assets/scss/archive-later.scss new file mode 100644 index 00000000000..8323ec57a3e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Assets/scss/archive-later.scss @@ -0,0 +1,3 @@ +.btn-archive-later { + white-space: nowrap; +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Drivers/ArchiveLaterPartDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Drivers/ArchiveLaterPartDisplayDriver.cs new file mode 100644 index 00000000000..b30159ff5a6 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Drivers/ArchiveLaterPartDisplayDriver.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using OrchardCore.ArchiveLater.Models; +using OrchardCore.ArchiveLater.ViewModels; +using OrchardCore.ContentManagement.Display.ContentDisplay; +using OrchardCore.ContentManagement.Display.Models; +using OrchardCore.Contents; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Modules; + +namespace OrchardCore.ArchiveLater.Drivers; + +public class ArchiveLaterPartDisplayDriver : ContentPartDisplayDriver +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + private readonly ILocalClock _localClock; + + public ArchiveLaterPartDisplayDriver( + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService, + ILocalClock localClock) + { + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + _localClock = localClock; + } + + public override IDisplayResult Display(ArchiveLaterPart part, BuildPartDisplayContext context) + => Initialize( + $"{nameof(ArchiveLaterPart)}_SummaryAdmin", + model => PopulateViewModel(part, model)).Location("SummaryAdmin", "Meta:25"); + + public override IDisplayResult Edit(ArchiveLaterPart part, BuildPartEditorContext context) + => Initialize( + GetEditorShapeType(context), + model => PopulateViewModel(part, model)).Location("Actions:10"); + + public override async Task UpdateAsync(ArchiveLaterPart part, IUpdateModel updater, UpdatePartEditorContext context) + { + var httpContext = _httpContextAccessor.HttpContext; + + if (await _authorizationService.AuthorizeAsync(httpContext?.User, CommonPermissions.PublishContent, part.ContentItem)) + { + var viewModel = new ArchiveLaterPartViewModel(); + + await updater.TryUpdateModelAsync(viewModel, Prefix); + + if (viewModel.ScheduledArchiveLocalDateTime == null || httpContext.Request.Form["submit.Save"] == "submit.CancelArchiveLater") + { + part.ScheduledArchiveUtc = null; + } + else + { + part.ScheduledArchiveUtc = await _localClock.ConvertToUtcAsync(viewModel.ScheduledArchiveLocalDateTime.Value); + } + } + + return Edit(part, context); + } + + private async ValueTask PopulateViewModel(ArchiveLaterPart part, ArchiveLaterPartViewModel viewModel) + { + viewModel.ContentItem = part.ContentItem; + viewModel.ScheduledArchiveUtc = part.ScheduledArchiveUtc; + viewModel.ScheduledArchiveLocalDateTime = part.ScheduledArchiveUtc.HasValue + ? (await _localClock.ConvertToLocalAsync(part.ScheduledArchiveUtc.Value)).DateTime + : (DateTime?)null; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Indexes/ArchiveLaterPartIndex.cs b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Indexes/ArchiveLaterPartIndex.cs new file mode 100644 index 00000000000..c4720376e19 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Indexes/ArchiveLaterPartIndex.cs @@ -0,0 +1,15 @@ +using System; +using YesSql.Indexes; + +namespace OrchardCore.ArchiveLater.Indexes; + +public class ArchiveLaterPartIndex : MapIndex +{ + public string ContentItemId { get; set; } + + public DateTime? ScheduledArchiveDateTimeUtc { get; set; } + + public bool Published { get; set; } + + public bool Latest { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Indexes/ArchiveLaterPartIndexProvider.cs b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Indexes/ArchiveLaterPartIndexProvider.cs new file mode 100644 index 00000000000..c60ab08cae6 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Indexes/ArchiveLaterPartIndexProvider.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.ArchiveLater.Models; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Handlers; +using OrchardCore.ContentManagement.Metadata; +using OrchardCore.Data; +using YesSql.Indexes; + +namespace OrchardCore.ArchiveLater.Indexes; + +public class ArchiveLaterPartIndexProvider : ContentHandlerBase, IIndexProvider, IScopedIndexProvider +{ + private readonly IServiceProvider _serviceProvider; + private readonly HashSet _partRemoved = new(); + private IContentDefinitionManager _contentDefinitionManager; + + public ArchiveLaterPartIndexProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public override Task UpdatedAsync(UpdateContentContext context) + { + var part = context.ContentItem.As(); + + if (part != null) + { + _contentDefinitionManager ??= _serviceProvider.GetRequiredService(); + + var contentTypeDefinition = _contentDefinitionManager.GetTypeDefinition(context.ContentItem.ContentType); + if (!contentTypeDefinition.Parts.Any(pd => pd.Name == nameof(ArchiveLaterPart))) + { + context.ContentItem.Remove(); + _partRemoved.Add(context.ContentItem.ContentItemId); + } + } + + return Task.CompletedTask; + } + + public string CollectionName { get; set; } + + public Type ForType() => typeof(ContentItem); + + public void Describe(IDescriptor context) => Describe((DescribeContext)context); + + public void Describe(DescribeContext context) + { + context + .For() + .When(contentItem => contentItem.Has() || _partRemoved.Contains(contentItem.ContentItemId)) + .Map(contentItem => + { + if (!contentItem.Published || !contentItem.Latest) + { + return null; + } + + var part = contentItem.As(); + if (part == null || !part.ScheduledArchiveUtc.HasValue) + { + return null; + } + + return new ArchiveLaterPartIndex + { + ContentItemId = part.ContentItem.ContentItemId, + ScheduledArchiveDateTimeUtc = part.ScheduledArchiveUtc, + Published = part.ContentItem.Published, + Latest = part.ContentItem.Latest + }; + }); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Manifest.cs new file mode 100644 index 00000000000..39d7c81adac --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Manifest.cs @@ -0,0 +1,11 @@ +using OrchardCore.Modules.Manifest; + +[assembly: Module( + Name = "Archive Later", + Author = ManifestConstants.OrchardCoreTeam, + Website = ManifestConstants.OrchardCoreWebsite, + Version = ManifestConstants.OrchardCoreVersion, + Description = "The Archive Later module adds the ability to schedule content items to be archived at a given future date and time.", + Dependencies = new[] { "OrchardCore.Contents" }, + Category = "Content Management" +)] diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Migrations.cs b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Migrations.cs new file mode 100644 index 00000000000..6d053390601 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Migrations.cs @@ -0,0 +1,46 @@ +using System; +using OrchardCore.ArchiveLater.Indexes; +using OrchardCore.ArchiveLater.Models; +using OrchardCore.ContentManagement.Metadata; +using OrchardCore.ContentManagement.Metadata.Settings; +using OrchardCore.ContentManagement.Records; +using OrchardCore.Data.Migration; +using YesSql.Sql; + +namespace OrchardCore.ArchiveLater; + +public class Migrations : DataMigration +{ + private readonly IContentDefinitionManager _contentDefinitionManager; + + public Migrations(IContentDefinitionManager contentDefinitionManager) + { + _contentDefinitionManager = contentDefinitionManager; + } + + public int Create() + { + _contentDefinitionManager.AlterPartDefinition(nameof(ArchiveLaterPart), builder => builder + .Attachable() + .WithDescription("Adds the ability to schedule content items to be archived at a given future date and time.")); + + SchemaBuilder.CreateMapIndexTable(table => table + .Column(nameof(ArchiveLaterPartIndex.ContentItemId)) + .Column(nameof(ArchiveLaterPartIndex.ScheduledArchiveDateTimeUtc)) + .Column(nameof(ArchiveLaterPartIndex.Published)) + .Column(nameof(ArchiveLaterPartIndex.Latest)) + ); + + SchemaBuilder.AlterIndexTable(table => table + .CreateIndex($"IDX_{nameof(ArchiveLaterPartIndex)}_{nameof(ContentItemIndex.DocumentId)}", + "Id", + nameof(ContentItemIndex.DocumentId), + nameof(ArchiveLaterPartIndex.ContentItemId), + nameof(ArchiveLaterPartIndex.ScheduledArchiveDateTimeUtc), + nameof(ArchiveLaterPartIndex.Published), + nameof(ArchiveLaterPartIndex.Latest)) + ); + + return 1; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Models/ArchiveLaterPart.cs b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Models/ArchiveLaterPart.cs new file mode 100644 index 00000000000..46abb928147 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Models/ArchiveLaterPart.cs @@ -0,0 +1,9 @@ +using System; +using OrchardCore.ContentManagement; + +namespace OrchardCore.ArchiveLater.Models; + +public class ArchiveLaterPart : ContentPart +{ + public DateTime? ScheduledArchiveUtc { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/OrchardCore.ArchiveLater.csproj b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/OrchardCore.ArchiveLater.csproj new file mode 100644 index 00000000000..e5c2ea6fd55 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/OrchardCore.ArchiveLater.csproj @@ -0,0 +1,28 @@ + + + + true + + OrchardCore AchiveLater + $(OCCMSDescription) + + The Archive Later module adds the ability to schedule content items to be archived at a given future date and time. + $(PackageTags) OrchardCoreCMS + + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Services/ScheduledArchivingBackgroundTask.cs b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Services/ScheduledArchivingBackgroundTask.cs new file mode 100644 index 00000000000..48a87ad4085 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Services/ScheduledArchivingBackgroundTask.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OrchardCore.ArchiveLater.Indexes; +using OrchardCore.ArchiveLater.Models; +using OrchardCore.BackgroundTasks; +using OrchardCore.ContentManagement; +using OrchardCore.Entities; +using OrchardCore.Modules; +using YesSql; + +namespace OrchardCore.ArchiveLater.Services; + +[BackgroundTask(Schedule = "* * * * *", Description = "Archives content items when their scheduled archive date time arrives.")] +public class ScheduledArchivingBackgroundTask : IBackgroundTask +{ + private readonly ILogger _logger; + private readonly IClock _clock; + + public ScheduledArchivingBackgroundTask(ILogger logger, IClock clock) + { + _logger = logger; + _clock = clock; + } + + public async Task DoWorkAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + var itemsToArchive = await serviceProvider + .GetRequiredService() + .QueryIndex(index => index.Latest && index.Published && index.ScheduledArchiveDateTimeUtc < _clock.UtcNow) + .ListAsync(); + + if (!itemsToArchive.Any()) + { + return; + } + + var contentManager = serviceProvider.GetRequiredService(); + + foreach (var item in itemsToArchive) + { + var contentItem = await contentManager.GetAsync(item.ContentItemId); + + var part = contentItem.As(); + if (part != null) + { + part.ScheduledArchiveUtc = null; + } + + _logger.LogDebug("Archiving scheduled content item {ContentItemId}.", contentItem.ContentItemId); + + await contentManager.UnpublishAsync(contentItem); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Startup.cs b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Startup.cs new file mode 100644 index 00000000000..3ce9e8b3000 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Startup.cs @@ -0,0 +1,40 @@ +using Fluid; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.ArchiveLater.Drivers; +using OrchardCore.ArchiveLater.Indexes; +using OrchardCore.ArchiveLater.Models; +using OrchardCore.ArchiveLater.Services; +using OrchardCore.ArchiveLater.ViewModels; +using OrchardCore.BackgroundTasks; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Display.ContentDisplay; +using OrchardCore.ContentManagement.Handlers; +using OrchardCore.Data; +using OrchardCore.Data.Migration; +using OrchardCore.Modules; + +namespace OrchardCore.ArchiveLater; + +public class Startup : StartupBase +{ + + public override void ConfigureServices(IServiceCollection services) + { + services.Configure(o => + { + o.MemberAccessStrategy.Register(); + }); + + services + .AddContentPart() + .UseDisplayDriver(); + + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); + + services.AddSingleton(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/ViewModels/ArchiveLaterPartViewModel.cs b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/ViewModels/ArchiveLaterPartViewModel.cs new file mode 100644 index 00000000000..fa0065fe6a2 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/ViewModels/ArchiveLaterPartViewModel.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using OrchardCore.ContentManagement; + +namespace OrchardCore.ArchiveLater.ViewModels; + +public class ArchiveLaterPartViewModel +{ + [BindNever] + public ContentItem ContentItem { get; set; } + + public DateTime? ScheduledArchiveUtc { get; set; } + + public DateTime? ScheduledArchiveLocalDateTime { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/ArchiveLaterPart.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/ArchiveLaterPart.Edit.cshtml new file mode 100644 index 00000000000..40d82e2654b --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/ArchiveLaterPart.Edit.cshtml @@ -0,0 +1,21 @@ +@using Microsoft.AspNetCore.Authorization +@using OrchardCore.Contents +@model ArchiveLaterPartViewModel + +@inject IAuthorizationService AuthorizationService + + + +@if (await AuthorizationService.AuthorizeAsync(User, CommonPermissions.PublishContent, Model.ContentItem)) +{ +
+ + +
+ @if (Model.ScheduledArchiveUtc.HasValue) + { +
+ +
+ } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/ArchiveLaterPart.Option.cshtml b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/ArchiveLaterPart.Option.cshtml new file mode 100644 index 00000000000..83effa54557 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/ArchiveLaterPart.Option.cshtml @@ -0,0 +1,4 @@ +@{ + string currentEditor = Model.Editor; +} + diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/ArchiveLaterPart.SummaryAdmin.cshtml b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/ArchiveLaterPart.SummaryAdmin.cshtml new file mode 100644 index 00000000000..7100ec52a44 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/ArchiveLaterPart.SummaryAdmin.cshtml @@ -0,0 +1,8 @@ +@model ArchiveLaterPartViewModel + +@if (Model.ScheduledArchiveUtc.HasValue) +{ +
+ @T["Scheduled to be archived on {0}", (object)(await DisplayAsync(await New.DateTime(Utc: Model.ScheduledArchiveUtc)))] +
+} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/_ViewImports.cshtml b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/_ViewImports.cshtml new file mode 100644 index 00000000000..ed35c715e82 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/Views/_ViewImports.cshtml @@ -0,0 +1,6 @@ +@inherits OrchardCore.DisplayManagement.Razor.RazorPage +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, OrchardCore.DisplayManagement +@addTagHelper *, OrchardCore.ResourceManagement +@using OrchardCore.ArchiveLater.ViewModels +@using OrchardCore.DisplayManagement diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/wwwroot/Styles/archive-later.css b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/wwwroot/Styles/archive-later.css new file mode 100644 index 00000000000..2c366af41fc --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/wwwroot/Styles/archive-later.css @@ -0,0 +1,3 @@ +.btn-archive-later { + white-space: nowrap; +} diff --git a/src/OrchardCore.Modules/OrchardCore.ArchiveLater/wwwroot/Styles/archive-later.min.css b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/wwwroot/Styles/archive-later.min.css new file mode 100644 index 00000000000..1adc7c90001 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ArchiveLater/wwwroot/Styles/archive-later.min.css @@ -0,0 +1 @@ +.btn-archive-later{white-space:nowrap} diff --git a/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj b/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj index 7d181798c8e..5d1470b70c9 100644 --- a/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj +++ b/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj @@ -40,6 +40,7 @@ +