From c6bec870d3791cc936420155c20bd03fab5f14e6 Mon Sep 17 00:00:00 2001 From: Artem Dudarev Date: Thu, 25 Apr 2024 14:32:09 +0200 Subject: [PATCH] VCST-918: Add scheduling (#3) --- module.ignore | 1 + .../BackgroundJobs/IPushMessageJobService.cs | 8 + .../Extensions/PushMessageExtensions.cs | 15 ++ .../Extensions/SearchServiceExtensions.cs | 31 +++ .../Models/PushMessage.cs | 6 + .../Models/PushMessageSearchCriteria.cs | 4 + .../Models/PushMessageStatus.cs | 7 + .../ModuleConstants.cs | 50 +++-- .../20240422153122_AddStartDate.Designer.cs | 193 +++++++++++++++++ .../Migrations/20240422153122_AddStartDate.cs | 54 +++++ .../PushMessagesDbContextModelSnapshot.cs | 11 + .../20240422153131_AddStartDate.Designer.cs | 196 +++++++++++++++++ .../Migrations/20240422153131_AddStartDate.cs | 52 +++++ .../PushMessagesDbContextModelSnapshot.cs | 11 + .../20240417085811_AddStartDate.Designer.cs | 198 ++++++++++++++++++ .../Migrations/20240417085811_AddStartDate.cs | 52 +++++ .../PushMessagesDbContextModelSnapshot.cs | 11 + .../BackgroundJobs/PushMessageJobs.cs | 90 ++++++++ .../ObjectSettingEntryChangedEventHandler.cs | 29 +++ .../Models/PushMessageEntity.cs | 18 ++ .../Repositories/PushMessagesRepository.cs | 2 +- .../Services/PushMessageSearchService.cs | 12 ++ .../Services/PushMessageService.cs | 46 +++- .../VirtoCommerce.PushMessages.Data.csproj | 1 + .../ClearAllPushMessagesCommandHandler.cs | 43 ++-- .../MarkAllPushMessagesReadCommandHandler.cs | 45 ++-- ...MarkAllPushMessagesUnreadCommandHandler.cs | 45 ++-- .../PushMessageChangedEventHandler.cs | 10 +- .../api_client/virtocommerce.pushmessages.ts | 80 +++++++ .../composables/useMessageDetails/index.ts | 28 ++- .../composables/useMessageList/index.ts | 14 +- .../src/modules/push-messages/locales/en.json | 29 ++- .../push-messages/pages/messageDetails.ts | 30 ++- .../push-messages/pages/messageList.ts | 26 ++- .../Controllers/Api/PushMessageController.cs | 9 +- .../Localizations/en.PushMessages.json | 27 +-- src/VirtoCommerce.PushMessages.Web/Module.cs | 15 +- 37 files changed, 1354 insertions(+), 145 deletions(-) create mode 100644 src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IPushMessageJobService.cs create mode 100644 src/VirtoCommerce.PushMessages.Core/Extensions/PushMessageExtensions.cs create mode 100644 src/VirtoCommerce.PushMessages.Core/Extensions/SearchServiceExtensions.cs create mode 100644 src/VirtoCommerce.PushMessages.Core/Models/PushMessageStatus.cs create mode 100644 src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240422153122_AddStartDate.Designer.cs create mode 100644 src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240422153122_AddStartDate.cs create mode 100644 src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240422153131_AddStartDate.Designer.cs create mode 100644 src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240422153131_AddStartDate.cs create mode 100644 src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240417085811_AddStartDate.Designer.cs create mode 100644 src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240417085811_AddStartDate.cs create mode 100644 src/VirtoCommerce.PushMessages.Data/BackgroundJobs/PushMessageJobs.cs create mode 100644 src/VirtoCommerce.PushMessages.Data/Handlers/ObjectSettingEntryChangedEventHandler.cs diff --git a/module.ignore b/module.ignore index e740d75..0ec9850 100644 --- a/module.ignore +++ b/module.ignore @@ -9,6 +9,7 @@ GraphQL.Server.Core.dll GraphQL.Server.Transports.AspNetCore.dll GraphQL.Server.Transports.AspNetCore.NewtonsoftJson.dll GraphQL.Server.Transports.Subscriptions.Abstractions.dll +GraphQL.Server.Transports.Subscriptions.WebSockets.dll GraphQL.SystemReactive.dll GraphQL-Parser.dll MediatR.dll diff --git a/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IPushMessageJobService.cs b/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IPushMessageJobService.cs new file mode 100644 index 0000000..5fd6b77 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/BackgroundJobs/IPushMessageJobService.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace VirtoCommerce.PushMessages.Core.BackgroundJobs; + +public interface IPushMessageJobService +{ + Task StartStopRecurringJobs(); +} diff --git a/src/VirtoCommerce.PushMessages.Core/Extensions/PushMessageExtensions.cs b/src/VirtoCommerce.PushMessages.Core/Extensions/PushMessageExtensions.cs new file mode 100644 index 0000000..3936bfe --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Extensions/PushMessageExtensions.cs @@ -0,0 +1,15 @@ +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.PushMessages.Core.Models; + +namespace VirtoCommerce.PushMessages.Core.Extensions; + +public static class PushMessageExtensions +{ + public static bool IsSent(this GenericChangedEntry entry) + { + return entry.NewEntry.Status == PushMessageStatus.Sent && + (entry.EntryState == EntryState.Added || + entry.EntryState == EntryState.Modified && entry.OldEntry.Status != PushMessageStatus.Sent); + } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Extensions/SearchServiceExtensions.cs b/src/VirtoCommerce.PushMessages.Core/Extensions/SearchServiceExtensions.cs new file mode 100644 index 0000000..3095105 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Extensions/SearchServiceExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.GenericCrud; + +namespace VirtoCommerce.PushMessages.Core.Extensions +{ + public static class SearchServiceExtensions + { + public static async Task SearchWhileResultIsNotEmpty(this ISearchService searchService, TCriteria searchCriteria, Func action) + where TCriteria : SearchCriteriaBase + where TResult : GenericSearchResult + where TModel : IEntity + { + TResult searchResult; + + do + { + searchResult = await searchService.SearchAsync(searchCriteria); + + if (searchResult.Results.Count > 0) + { + await action(searchResult); + } + } + while (searchCriteria.Take > 0 && + searchResult.Results.Count == searchCriteria.Take && + searchResult.Results.Count != searchResult.TotalCount); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Core/Models/PushMessage.cs b/src/VirtoCommerce.PushMessages.Core/Models/PushMessage.cs index b2640ca..221faf6 100644 --- a/src/VirtoCommerce.PushMessages.Core/Models/PushMessage.cs +++ b/src/VirtoCommerce.PushMessages.Core/Models/PushMessage.cs @@ -7,8 +7,14 @@ namespace VirtoCommerce.PushMessages.Core.Models; public class PushMessage : AuditableEntity, ICloneable { + public string Topic { get; set; } + public string ShortMessage { get; set; } + public DateTime? StartDate { get; set; } + + public string Status { get; set; } + public IList MemberIds { get; set; } [JsonIgnore] diff --git a/src/VirtoCommerce.PushMessages.Core/Models/PushMessageSearchCriteria.cs b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageSearchCriteria.cs index 6e9c943..dfd825d 100644 --- a/src/VirtoCommerce.PushMessages.Core/Models/PushMessageSearchCriteria.cs +++ b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageSearchCriteria.cs @@ -1,7 +1,11 @@ +using System; +using System.Collections.Generic; using VirtoCommerce.Platform.Core.Common; namespace VirtoCommerce.PushMessages.Core.Models; public class PushMessageSearchCriteria : SearchCriteriaBase { + public DateTime? StartDateBefore { get; set; } + public IList Statuses { get; set; } } diff --git a/src/VirtoCommerce.PushMessages.Core/Models/PushMessageStatus.cs b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageStatus.cs new file mode 100644 index 0000000..bcde9f7 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Core/Models/PushMessageStatus.cs @@ -0,0 +1,7 @@ +namespace VirtoCommerce.PushMessages.Core.Models; + +public static class PushMessageStatus +{ + public const string Scheduled = "Scheduled"; + public const string Sent = "Sent"; +} diff --git a/src/VirtoCommerce.PushMessages.Core/ModuleConstants.cs b/src/VirtoCommerce.PushMessages.Core/ModuleConstants.cs index 038808d..b203cc7 100644 --- a/src/VirtoCommerce.PushMessages.Core/ModuleConstants.cs +++ b/src/VirtoCommerce.PushMessages.Core/ModuleConstants.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using VirtoCommerce.Platform.Core.Settings; namespace VirtoCommerce.PushMessages.Core; @@ -16,13 +17,13 @@ public static class Permissions public const string Delete = "PushMessages:delete"; public static string[] AllPermissions { get; } = - { + [ Access, Create, Read, Update, Delete, - }; + ]; } } @@ -30,28 +31,47 @@ public static class Settings { public static class General { - public static SettingDescriptor PushMessagesEnabled { get; } = new() + public static SettingDescriptor BatchSize { get; } = new() + { + Name = "PushMessages.BatchSize", + GroupName = "Push Messages|General", + ValueType = SettingValueType.Integer, + DefaultValue = 50, + }; + + public static IEnumerable AllGeneralSettings { - Name = "PushMessages.PushMessagesEnabled", - GroupName = "PushMessages|General", + get + { + yield return BatchSize; + } + } + } + + public static class BackgroundJobs + { + public static SettingDescriptor Enable { get; } = new() + { + Name = "PushMessages.BackgroundJobs.Enable", + GroupName = "Push Messages|Background Jobs", ValueType = SettingValueType.Boolean, - DefaultValue = false, + DefaultValue = true, }; - public static SettingDescriptor PushMessagesPassword { get; } = new() + public static SettingDescriptor CronExpression { get; } = new() { - Name = "PushMessages.PushMessagesPassword", - GroupName = "PushMessages|Advanced", - ValueType = SettingValueType.SecureString, - DefaultValue = "qwerty", + Name = "PushMessages.BackgroundJobs.CronExpression", + GroupName = "Push Messages|Background Jobs", + ValueType = SettingValueType.ShortText, + DefaultValue = "0/5 * * * *", }; - public static IEnumerable AllGeneralSettings + public static IEnumerable AllBackgroundJobsSettings { get { - yield return PushMessagesEnabled; - yield return PushMessagesPassword; + yield return Enable; + yield return CronExpression; } } } @@ -60,7 +80,7 @@ public static IEnumerable AllSettings { get { - return General.AllGeneralSettings; + return General.AllGeneralSettings.Concat(BackgroundJobs.AllBackgroundJobsSettings); } } } diff --git a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240422153122_AddStartDate.Designer.cs b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240422153122_AddStartDate.Designer.cs new file mode 100644 index 0000000..747a55d --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240422153122_AddStartDate.Designer.cs @@ -0,0 +1,193 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using VirtoCommerce.PushMessages.Data.Repositories; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.MySql.Migrations +{ + [DbContext(typeof(PushMessagesDbContext))] + [Migration("20240422153122_AddStartDate")] + partial class AddStartDate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.Property("ShortMessage") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.HasKey("Id"); + + b.ToTable("PushMessage", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "MemberId") + .IsUnique() + .HasDatabaseName("IX_PushMessageMember_MessageId_MemberId"); + + b.ToTable("PushMessageMember", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime(6)"); + + b.Property("IsHidden") + .HasColumnType("tinyint(1)"); + + b.Property("IsRead") + .HasColumnType("tinyint(1)"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("MemberName") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageRecipient_MessageId_UserId"); + + b.ToTable("PushMessageRecipient", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Members") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Recipients") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Navigation("Members"); + + b.Navigation("Recipients"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240422153122_AddStartDate.cs b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240422153122_AddStartDate.cs new file mode 100644 index 0000000..26042a3 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/20240422153122_AddStartDate.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.MySql.Migrations +{ + /// + public partial class AddStartDate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "StartDate", + table: "PushMessage", + type: "datetime(6)", + nullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "PushMessage", + type: "varchar(64)", + maxLength: 64, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Topic", + table: "PushMessage", + type: "varchar(128)", + maxLength: 128, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.Sql("UPDATE PushMessage SET Status = 'Sent';"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "StartDate", + table: "PushMessage"); + + migrationBuilder.DropColumn( + name: "Status", + table: "PushMessage"); + + migrationBuilder.DropColumn( + name: "Topic", + table: "PushMessage"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs index fe565ce..77dc2fc 100644 --- a/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs +++ b/src/VirtoCommerce.PushMessages.Data.MySql/Migrations/PushMessagesDbContextModelSnapshot.cs @@ -44,6 +44,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(1024) .HasColumnType("varchar(1024)"); + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + b.HasKey("Id"); b.ToTable("PushMessage", (string)null); diff --git a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240422153131_AddStartDate.Designer.cs b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240422153131_AddStartDate.Designer.cs new file mode 100644 index 0000000..561d0b9 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240422153131_AddStartDate.Designer.cs @@ -0,0 +1,196 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using VirtoCommerce.PushMessages.Data.Repositories; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.PostgreSql.Migrations +{ + [DbContext(typeof(PushMessagesDbContext))] + [Migration("20240422153131_AddStartDate")] + partial class AddStartDate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ShortMessage") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.ToTable("PushMessage", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "MemberId") + .IsUnique() + .HasDatabaseName("IX_PushMessageMember_MessageId_MemberId"); + + b.ToTable("PushMessageMember", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsHidden") + .HasColumnType("boolean"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("MemberName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageRecipient_MessageId_UserId"); + + b.ToTable("PushMessageRecipient", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Members") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Recipients") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Navigation("Members"); + + b.Navigation("Recipients"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240422153131_AddStartDate.cs b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240422153131_AddStartDate.cs new file mode 100644 index 0000000..fadc8b0 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/20240422153131_AddStartDate.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.PostgreSql.Migrations +{ + /// + public partial class AddStartDate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "StartDate", + table: "PushMessage", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "PushMessage", + type: "character varying(64)", + maxLength: 64, + nullable: true); + + migrationBuilder.AddColumn( + name: "Topic", + table: "PushMessage", + type: "character varying(128)", + maxLength: 128, + nullable: true); + + migrationBuilder.Sql("UPDATE \"PushMessage\" SET \"Status\" = 'Sent'"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "StartDate", + table: "PushMessage"); + + migrationBuilder.DropColumn( + name: "Status", + table: "PushMessage"); + + migrationBuilder.DropColumn( + name: "Topic", + table: "PushMessage"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs index 0c0efa9..3e07fc4 100644 --- a/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs +++ b/src/VirtoCommerce.PushMessages.Data.PostgreSql/Migrations/PushMessagesDbContextModelSnapshot.cs @@ -47,6 +47,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(1024) .HasColumnType("character varying(1024)"); + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + b.HasKey("Id"); b.ToTable("PushMessage", (string)null); diff --git a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240417085811_AddStartDate.Designer.cs b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240417085811_AddStartDate.Designer.cs new file mode 100644 index 0000000..c038119 --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240417085811_AddStartDate.Designer.cs @@ -0,0 +1,198 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using VirtoCommerce.PushMessages.Data.Repositories; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.SqlServer.Migrations +{ + [DbContext(typeof(PushMessagesDbContext))] + [Migration("20240417085811_AddStartDate")] + partial class AddStartDate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("ShortMessage") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.HasKey("Id"); + + b.ToTable("PushMessage", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "MemberId") + .IsUnique() + .HasDatabaseName("IX_PushMessageMember_MessageId_MemberId") + .HasFilter("[MemberId] IS NOT NULL"); + + b.ToTable("PushMessageMember", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("IsHidden") + .HasColumnType("bit"); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("MemberId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MemberName") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("MessageId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ModifiedBy") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedDate") + .HasColumnType("datetime2"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId") + .IsUnique() + .HasDatabaseName("IX_PushMessageRecipient_MessageId_UserId") + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PushMessageRecipient", (string)null); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageMemberEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Members") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageRecipientEntity", b => + { + b.HasOne("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", "Message") + .WithMany("Recipients") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("VirtoCommerce.PushMessages.Data.Models.PushMessageEntity", b => + { + b.Navigation("Members"); + + b.Navigation("Recipients"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240417085811_AddStartDate.cs b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240417085811_AddStartDate.cs new file mode 100644 index 0000000..3016b5b --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/20240417085811_AddStartDate.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace VirtoCommerce.PushMessages.Data.SqlServer.Migrations +{ + /// + public partial class AddStartDate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "StartDate", + table: "PushMessage", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "PushMessage", + type: "nvarchar(64)", + maxLength: 64, + nullable: true); + + migrationBuilder.AddColumn( + name: "Topic", + table: "PushMessage", + type: "nvarchar(128)", + maxLength: 128, + nullable: true); + + migrationBuilder.Sql("UPDATE PushMessage SET Status = 'Sent';"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "StartDate", + table: "PushMessage"); + + migrationBuilder.DropColumn( + name: "Status", + table: "PushMessage"); + + migrationBuilder.DropColumn( + name: "Topic", + table: "PushMessage"); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs index 0f09d84..c58ceb7 100644 --- a/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs +++ b/src/VirtoCommerce.PushMessages.Data.SqlServer/Migrations/PushMessagesDbContextModelSnapshot.cs @@ -47,6 +47,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(1024) .HasColumnType("nvarchar(1024)"); + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Topic") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + b.HasKey("Id"); b.ToTable("PushMessage", (string)null); diff --git a/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/PushMessageJobs.cs b/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/PushMessageJobs.cs new file mode 100644 index 0000000..23a6afb --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data/BackgroundJobs/PushMessageJobs.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Hangfire; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.PushMessages.Core.BackgroundJobs; +using VirtoCommerce.PushMessages.Core.Extensions; +using VirtoCommerce.PushMessages.Core.Models; +using VirtoCommerce.PushMessages.Core.Services; +using GeneralSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.General; +using JobSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.BackgroundJobs; + +namespace VirtoCommerce.PushMessages.Data.BackgroundJobs; + +public class PushMessageJobs : IPushMessageJobService +{ + private static readonly MethodInfo _recurringJobMethod = typeof(PushMessageJobs).GetMethod(nameof(SendScheduledMessages)); + + private readonly IPushMessageService _crudService; + private readonly IPushMessageSearchService _searchService; + private readonly ISettingsManager _settingsManager; + + public PushMessageJobs( + IPushMessageService crudService, + IPushMessageSearchService searchService, + ISettingsManager settingsManager) + { + _crudService = crudService; + _searchService = searchService; + _settingsManager = settingsManager; + } + + [DisableConcurrentExecution(10)] + public async Task SendScheduledMessages(IJobCancellationToken cancellationToken) + { + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.StartDateBefore = DateTime.UtcNow; + searchCriteria.Statuses = [PushMessageStatus.Scheduled]; + searchCriteria.Sort = $"{nameof(PushMessage.StartDate)};{nameof(PushMessage.CreatedDate)}"; + searchCriteria.Take = await _settingsManager.GetValueAsync(GeneralSettings.BatchSize); + + await _searchService.SearchWhileResultIsNotEmpty(searchCriteria, async searchResult => + { + cancellationToken.ThrowIfCancellationRequested(); + + searchResult.Results.Apply(x => x.Status = PushMessageStatus.Sent); + await _crudService.SaveChangesAsync(searchResult.Results); + }); + } + + public async Task StartStopRecurringJobs() + { + const string recurringJobId = $"{nameof(PushMessageJobs)}.{nameof(SendScheduledMessages)}"; + var enableJobs = await _settingsManager.GetValueAsync(JobSettings.Enable); + + if (enableJobs) + { + var cronExpression = await _settingsManager.GetValueAsync(JobSettings.CronExpression); + RecurringJob.AddOrUpdate(recurringJobId, x => x.SendScheduledMessages(JobCancellationToken.Null), cronExpression); + } + else + { + CancelProcessingJobs(_recurringJobMethod); + RecurringJob.RemoveIfExists(recurringJobId); + } + } + + + private static void CancelProcessingJobs(MethodInfo method) + { + var processingJobs = JobStorage.Current.GetMonitoringApi().ProcessingJobs(0, int.MaxValue); + var (jobId, _) = processingJobs.FirstOrDefault(x => x.Value?.Job?.Method == method); + + if (string.IsNullOrEmpty(jobId)) + { + return; + } + + try + { + BackgroundJob.Delete(jobId); + } + catch + { + // Ignore concurrency exceptions, when somebody else cancelled it as well. + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data/Handlers/ObjectSettingEntryChangedEventHandler.cs b/src/VirtoCommerce.PushMessages.Data/Handlers/ObjectSettingEntryChangedEventHandler.cs new file mode 100644 index 0000000..7da9d8c --- /dev/null +++ b/src/VirtoCommerce.PushMessages.Data/Handlers/ObjectSettingEntryChangedEventHandler.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Threading.Tasks; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Events; +using VirtoCommerce.Platform.Core.Settings.Events; +using VirtoCommerce.PushMessages.Core.BackgroundJobs; +using JobSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.BackgroundJobs; + +namespace VirtoCommerce.PushMessages.Data.Handlers; + +public class ObjectSettingEntryChangedEventHandler : IEventHandler +{ + private readonly IPushMessageJobService _pushMessageJobService; + + public ObjectSettingEntryChangedEventHandler(IPushMessageJobService pushMessageJobService) + { + _pushMessageJobService = pushMessageJobService; + } + + public virtual async Task Handle(ObjectSettingChangedEvent message) + { + if (message.ChangedEntries.Any(x => x.EntryState is EntryState.Modified or EntryState.Added && + (x.NewEntry.Name == JobSettings.Enable.Name || + x.NewEntry.Name == JobSettings.CronExpression.Name))) + { + await _pushMessageJobService.StartStopRecurringJobs(); + } + } +} diff --git a/src/VirtoCommerce.PushMessages.Data/Models/PushMessageEntity.cs b/src/VirtoCommerce.PushMessages.Data/Models/PushMessageEntity.cs index d2f299a..513a162 100644 --- a/src/VirtoCommerce.PushMessages.Data/Models/PushMessageEntity.cs +++ b/src/VirtoCommerce.PushMessages.Data/Models/PushMessageEntity.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -9,9 +10,17 @@ namespace VirtoCommerce.PushMessages.Data.Models; public class PushMessageEntity : AuditableEntity, IDataEntity { + [StringLength(128)] + public string Topic { get; set; } + [StringLength(1024)] public string ShortMessage { get; set; } + public DateTime? StartDate { get; set; } + + [StringLength(64)] + public string Status { get; set; } + public virtual ObservableCollection Members { get; set; } = new NullCollection(); public virtual ObservableCollection Recipients { get; set; } = new NullCollection(); @@ -24,7 +33,10 @@ public virtual PushMessage ToModel(PushMessage model) model.ModifiedBy = ModifiedBy; model.ModifiedDate = ModifiedDate; + model.Topic = Topic; model.ShortMessage = ShortMessage; + model.StartDate = StartDate; + model.Status = Status; model.MemberIds = Members.OrderBy(x => x.MemberId).Select(x => x.MemberId).ToList(); return model; @@ -40,7 +52,10 @@ public virtual PushMessageEntity FromModel(PushMessage model, PrimaryKeyResolvin ModifiedBy = model.ModifiedBy; ModifiedDate = model.ModifiedDate; + Topic = model.Topic; ShortMessage = model.ShortMessage; + StartDate = model.StartDate; + Status = model.Status; if (model.MemberIds != null) { @@ -56,7 +71,10 @@ public virtual PushMessageEntity FromModel(PushMessage model, PrimaryKeyResolvin public virtual void Patch(PushMessageEntity target) { + target.Topic = Topic; target.ShortMessage = ShortMessage; + target.StartDate = StartDate; + target.Status = Status; if (!Members.IsNullCollection()) { diff --git a/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesRepository.cs b/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesRepository.cs index 7c27685..81c8c04 100644 --- a/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesRepository.cs +++ b/src/VirtoCommerce.PushMessages.Data/Repositories/PushMessagesRepository.cs @@ -36,7 +36,7 @@ public virtual async Task> GetMessagesByIdsAsync(IList< if (messages.Count > 0) { - var responseGroupEnum = EnumUtility.SafeParseFlags(responseGroup, PushMessageResponseGroup.None); + var responseGroupEnum = EnumUtility.SafeParseFlags(responseGroup, PushMessageResponseGroup.Full); if (responseGroupEnum.HasFlag(PushMessageResponseGroup.WithMembers)) { diff --git a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageSearchService.cs b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageSearchService.cs index df7d97a..b13de1f 100644 --- a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageSearchService.cs +++ b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageSearchService.cs @@ -35,6 +35,18 @@ protected override IQueryable BuildQuery(IRepository reposito x.Id.Contains(criteria.Keyword)); } + if (criteria.StartDateBefore != null) + { + query = query.Where(x => x.StartDate != null && x.StartDate <= criteria.StartDateBefore); + } + + if (!criteria.Statuses.IsNullOrEmpty()) + { + query = criteria.Statuses.Count == 1 + ? query.Where(x => x.Status == criteria.Statuses.First()) + : query.Where(x => criteria.Statuses.Contains(x.Status)); + } + return query; } diff --git a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs index b0ec61e..1a93ebd 100644 --- a/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs +++ b/src/VirtoCommerce.PushMessages.Data/Services/PushMessageService.cs @@ -12,6 +12,7 @@ using VirtoCommerce.Platform.Core.Security; using VirtoCommerce.Platform.Data.GenericCrud; using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Extensions; using VirtoCommerce.PushMessages.Core.Models; using VirtoCommerce.PushMessages.Core.Services; using VirtoCommerce.PushMessages.Data.Extensions; @@ -40,6 +41,21 @@ public PushMessageService( _recipientService = recipientService; } + public override async Task SaveChangesAsync(IList models) + { + var ids = models.Where(x => x.Id != null).Select(x => x.Id).ToList(); + var existingModels = await CheckStatusAsync(ids); + UpdateStatus(models, existingModels); + await base.SaveChangesAsync(models); + } + + public override async Task DeleteAsync(IList ids, bool softDelete = false) + { + await CheckStatusAsync(ids); + await base.DeleteAsync(ids, softDelete); + } + + protected override Task> LoadEntities(IRepository repository, IList ids, string responseGroup) { return ((IPushMessagesRepository)repository).GetMessagesByIdsAsync(ids, responseGroup); @@ -59,10 +75,38 @@ protected override void ClearCache(IList models) GenericSearchCachingRegion.ExpireRegion(); } + + private async Task> CheckStatusAsync(IList ids) + { + var models = await GetAsync(ids); + + if (models.Any(x => x.Status == PushMessageStatus.Sent)) + { + throw new InvalidOperationException($"Cannot modify or delete messages with status {PushMessageStatus.Sent}."); + } + + return models; + } + + private static void UpdateStatus(IList newModels, IList oldModels) + { + foreach (var newModel in newModels) + { + var oldModel = oldModels.FirstOrDefault(x => x.Id.EqualsInvariant(newModel.Id)); + + if (oldModel is null) + { + newModel.Status = newModel.StartDate > DateTime.UtcNow + ? PushMessageStatus.Scheduled + : PushMessageStatus.Sent; + } + } + } + private async Task AddRecipients(IList> changedEntries) { foreach (var changedEntry in changedEntries.Where(x => - x.EntryState == EntryState.Added && + x.IsSent() && x.NewEntry.MemberIds.Count > 0)) { var message = changedEntry.NewEntry; diff --git a/src/VirtoCommerce.PushMessages.Data/VirtoCommerce.PushMessages.Data.csproj b/src/VirtoCommerce.PushMessages.Data/VirtoCommerce.PushMessages.Data.csproj index e1ef701..943ae24 100644 --- a/src/VirtoCommerce.PushMessages.Data/VirtoCommerce.PushMessages.Data.csproj +++ b/src/VirtoCommerce.PushMessages.Data/VirtoCommerce.PushMessages.Data.csproj @@ -10,6 +10,7 @@ + diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/ClearAllPushMessagesCommandHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/ClearAllPushMessagesCommandHandler.cs index b58befd..51b17fd 100644 --- a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/ClearAllPushMessagesCommandHandler.cs +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/ClearAllPushMessagesCommandHandler.cs @@ -2,8 +2,11 @@ using System.Threading.Tasks; using MediatR; using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.PushMessages.Core.Extensions; using VirtoCommerce.PushMessages.Core.Models; using VirtoCommerce.PushMessages.Core.Services; +using GeneralSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.General; namespace VirtoCommerce.PushMessages.ExperienceApi.Commands { @@ -11,48 +14,32 @@ public class ClearAllPushMessagesCommandHandler : IRequestHandler Handle(ClearAllPushMessageCommand request, CancellationToken cancellationToken) { - var searchCriteria = GetSearchCriteria(request); - PushMessageRecipientSearchResult searchResult; + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.UserId = request.UserId; + searchCriteria.WithHidden = false; + searchCriteria.Take = await _settingsManager.GetValueAsync(GeneralSettings.BatchSize); - do + await _recipientSearchService.SearchWhileResultIsNotEmpty(searchCriteria, async searchResult => { - searchResult = await _recipientSearchService.SearchAsync(searchCriteria); - - if (searchResult.Results.Count > 0) - { - foreach (var recipient in searchResult.Results) - { - recipient.IsHidden = true; - } - - await _recipientService.SaveChangesAsync(searchResult.Results); - } - } - while (searchResult.Results.Count == searchCriteria.Take && - searchResult.Results.Count != searchResult.TotalCount); + searchResult.Results.Apply(x => x.IsHidden = true); + await _recipientService.SaveChangesAsync(searchResult.Results); + }); return true; } - - private static PushMessageRecipientSearchCriteria GetSearchCriteria(PushMessagesCommand request) - { - var criteria = AbstractTypeFactory.TryCreateInstance(); - criteria.UserId = request.UserId; - criteria.WithHidden = false; - criteria.Take = 50; - - return criteria; - } } } diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/MarkAllPushMessagesReadCommandHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/MarkAllPushMessagesReadCommandHandler.cs index ef286ce..fc87bc4 100644 --- a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/MarkAllPushMessagesReadCommandHandler.cs +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/MarkAllPushMessagesReadCommandHandler.cs @@ -2,8 +2,11 @@ using System.Threading.Tasks; using MediatR; using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.PushMessages.Core.Extensions; using VirtoCommerce.PushMessages.Core.Models; using VirtoCommerce.PushMessages.Core.Services; +using GeneralSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.General; namespace VirtoCommerce.PushMessages.ExperienceApi.Commands { @@ -11,49 +14,33 @@ public class MarkAllPushMessagesReadCommandHandler : IRequestHandler Handle(MarkAllPushMessagesReadCommand request, CancellationToken cancellationToken) { - var searchCriteria = GetSearchCriteria(request); - PushMessageRecipientSearchResult searchResult; + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.UserId = request.UserId; + searchCriteria.IsRead = false; + searchCriteria.WithHidden = true; + searchCriteria.Take = await _settingsManager.GetValueAsync(GeneralSettings.BatchSize); - do + await _recipientSearchService.SearchWhileResultIsNotEmpty(searchCriteria, async searchResult => { - searchResult = await _recipientSearchService.SearchAsync(searchCriteria); - - if (searchResult.Results.Count > 0) - { - foreach (var recipient in searchResult.Results) - { - recipient.IsRead = true; - } - - await _recipientService.SaveChangesAsync(searchResult.Results); - } - } - while (searchResult.Results.Count == searchCriteria.Take && - searchResult.Results.Count != searchResult.TotalCount); + searchResult.Results.Apply(x => x.IsRead = true); + await _recipientService.SaveChangesAsync(searchResult.Results); + }); return true; } - - private static PushMessageRecipientSearchCriteria GetSearchCriteria(MarkAllPushMessagesReadCommand request) - { - var criteria = AbstractTypeFactory.TryCreateInstance(); - criteria.UserId = request.UserId; - criteria.IsRead = false; - criteria.WithHidden = true; - criteria.Take = 50; - - return criteria; - } } } diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/MarkAllPushMessagesUnreadCommandHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/MarkAllPushMessagesUnreadCommandHandler.cs index f2bc0f7..defd0fa 100644 --- a/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/MarkAllPushMessagesUnreadCommandHandler.cs +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Commands/MarkAllPushMessagesUnreadCommandHandler.cs @@ -2,8 +2,11 @@ using System.Threading.Tasks; using MediatR; using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.PushMessages.Core.Extensions; using VirtoCommerce.PushMessages.Core.Models; using VirtoCommerce.PushMessages.Core.Services; +using GeneralSettings = VirtoCommerce.PushMessages.Core.ModuleConstants.Settings.General; namespace VirtoCommerce.PushMessages.ExperienceApi.Commands { @@ -11,49 +14,33 @@ public class MarkAllPushMessagesUnreadCommandHandler : IRequestHandler Handle(MarkAllPushMessagesUnreadCommand request, CancellationToken cancellationToken) { - var searchCriteria = GetSearchCriteria(request); - PushMessageRecipientSearchResult searchResult; + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.UserId = request.UserId; + searchCriteria.IsRead = true; + searchCriteria.WithHidden = true; + searchCriteria.Take = await _settingsManager.GetValueAsync(GeneralSettings.BatchSize); - do + await _recipientSearchService.SearchWhileResultIsNotEmpty(searchCriteria, async searchResult => { - searchResult = await _recipientSearchService.SearchAsync(searchCriteria); - - if (searchResult.Results.Count > 0) - { - foreach (var recipient in searchResult.Results) - { - recipient.IsRead = false; - } - - await _recipientService.SaveChangesAsync(searchResult.Results); - } - } - while (searchResult.Results.Count == searchCriteria.Take && - searchResult.Results.Count != searchResult.TotalCount); + searchResult.Results.Apply(x => x.IsRead = false); + await _recipientService.SaveChangesAsync(searchResult.Results); + }); return true; } - - private static PushMessageRecipientSearchCriteria GetSearchCriteria(PushMessagesCommand request) - { - var criteria = AbstractTypeFactory.TryCreateInstance(); - criteria.UserId = request.UserId; - criteria.IsRead = true; - criteria.WithHidden = true; - criteria.Take = 50; - - return criteria; - } } } diff --git a/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageChangedEventHandler.cs b/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageChangedEventHandler.cs index 7ae0c2b..532eb03 100644 --- a/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageChangedEventHandler.cs +++ b/src/VirtoCommerce.PushMessages.ExperienceApi/Handlers/PushMessageChangedEventHandler.cs @@ -1,6 +1,8 @@ +using System.Linq; using System.Threading.Tasks; using VirtoCommerce.Platform.Core.Events; using VirtoCommerce.PushMessages.Core.Events; +using VirtoCommerce.PushMessages.Core.Extensions; using VirtoCommerce.PushMessages.ExperienceApi.Models; using VirtoCommerce.PushMessages.ExperienceApi.Subscriptions; @@ -17,10 +19,12 @@ public PushMessageChangedEventHandler(IPushMessageHub eventBroker) public async Task Handle(PushMessageChangedEvent message) { - foreach (var entry in message.ChangedEntries) + foreach (var pushMessage in message.ChangedEntries + .Where(x => + x.IsSent() && + x.NewEntry.UserIds?.Count > 0) + .Select(x => x.NewEntry)) { - var pushMessage = entry.NewEntry; - foreach (var userId in pushMessage.UserIds) { var expPushMessage = new ExpPushMessage diff --git a/src/VirtoCommerce.PushMessages.Web/App/src/api_client/virtocommerce.pushmessages.ts b/src/VirtoCommerce.PushMessages.Web/App/src/api_client/virtocommerce.pushmessages.ts index c681645..060f33d 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/src/api_client/virtocommerce.pushmessages.ts +++ b/src/VirtoCommerce.PushMessages.Web/App/src/api_client/virtocommerce.pushmessages.ts @@ -197,6 +197,58 @@ export class PushMessageClient extends AuthApiBase { return Promise.resolve(null as any); } + /** + * @param body (optional) + * @return Success + */ + update(body?: PushMessage | undefined): Promise { + let url_ = this.baseUrl + "/api/push-message"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(body); + + let options_: RequestInit = { + body: content_, + method: "PUT", + headers: { + "Content-Type": "application/json-patch+json", + "Accept": "text/plain" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processUpdate(_response); + }); + } + + protected processUpdate(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = PushMessage.fromJS(resultData200); + return result200; + }); + } else if (status === 401) { + return response.text().then((_responseText) => { + return throwException("Unauthorized", status, _responseText, _headers); + }); + } else if (status === 403) { + return response.text().then((_responseText) => { + return throwException("Forbidden", status, _responseText, _headers); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + /** * @param ids (optional) * @return No Content @@ -302,7 +354,10 @@ export class PushMessageClient extends AuthApiBase { } export class PushMessage implements IPushMessage { + topic?: string | undefined; shortMessage?: string | undefined; + startDate?: Date | undefined; + status?: string | undefined; memberIds?: string[] | undefined; createdDate?: Date; modifiedDate?: Date | undefined; @@ -321,7 +376,10 @@ export class PushMessage implements IPushMessage { init(_data?: any) { if (_data) { + this.topic = _data["topic"]; this.shortMessage = _data["shortMessage"]; + this.startDate = _data["startDate"] ? new Date(_data["startDate"].toString()) : undefined; + this.status = _data["status"]; if (Array.isArray(_data["memberIds"])) { this.memberIds = [] as any; for (let item of _data["memberIds"]) @@ -344,7 +402,10 @@ export class PushMessage implements IPushMessage { toJSON(data?: any) { data = typeof data === 'object' ? data : {}; + data["topic"] = this.topic; data["shortMessage"] = this.shortMessage; + data["startDate"] = this.startDate ? this.startDate.toISOString() : undefined; + data["status"] = this.status; if (Array.isArray(this.memberIds)) { data["memberIds"] = []; for (let item of this.memberIds) @@ -360,7 +421,10 @@ export class PushMessage implements IPushMessage { } export interface IPushMessage { + topic?: string | undefined; shortMessage?: string | undefined; + startDate?: Date | undefined; + status?: string | undefined; memberIds?: string[] | undefined; createdDate?: Date; modifiedDate?: Date | undefined; @@ -626,6 +690,8 @@ export interface IPushMessageRecipientSearchResult { } export class PushMessageSearchCriteria implements IPushMessageSearchCriteria { + startDateBefore?: Date | undefined; + statuses?: string[] | undefined; responseGroup?: string | undefined; /** Search object type */ objectType?: string | undefined; @@ -653,6 +719,12 @@ export class PushMessageSearchCriteria implements IPushMessageSearchCriteria { init(_data?: any) { if (_data) { + this.startDateBefore = _data["startDateBefore"] ? new Date(_data["startDateBefore"].toString()) : undefined; + if (Array.isArray(_data["statuses"])) { + this.statuses = [] as any; + for (let item of _data["statuses"]) + this.statuses!.push(item); + } this.responseGroup = _data["responseGroup"]; this.objectType = _data["objectType"]; if (Array.isArray(_data["objectTypes"])) { @@ -688,6 +760,12 @@ export class PushMessageSearchCriteria implements IPushMessageSearchCriteria { toJSON(data?: any) { data = typeof data === 'object' ? data : {}; + data["startDateBefore"] = this.startDateBefore ? this.startDateBefore.toISOString() : undefined; + if (Array.isArray(this.statuses)) { + data["statuses"] = []; + for (let item of this.statuses) + data["statuses"].push(item); + } data["responseGroup"] = this.responseGroup; data["objectType"] = this.objectType; if (Array.isArray(this.objectTypes)) { @@ -716,6 +794,8 @@ export class PushMessageSearchCriteria implements IPushMessageSearchCriteria { } export interface IPushMessageSearchCriteria { + startDateBefore?: Date | undefined; + statuses?: string[] | undefined; responseGroup?: string | undefined; /** Search object type */ objectType?: string | undefined; diff --git a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/composables/useMessageDetails/index.ts b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/composables/useMessageDetails/index.ts index db1b8ba..38b1397 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/composables/useMessageDetails/index.ts +++ b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/composables/useMessageDetails/index.ts @@ -16,6 +16,7 @@ const { getApiClient: getPushMessageApiClient } = useApiClient(PushMessageClient export interface PushMessageDetailsScope extends DetailsBaseBladeScope { toolbarOverrides: { saveChanges: IBladeToolbar; + remove: IBladeToolbar; }; } @@ -24,6 +25,8 @@ export default (args: { emit: InstanceType["$emit"]; mounted: Ref; }) => { + const messageId = args.props.param; + const detailsFactory = useDetailsFactory({ load: async (message) => { if (message?.id) { @@ -31,10 +34,13 @@ export default (args: { } }, saveChanges: async (message) => { - return (await getPushMessageApiClient()).create(message); + const apiClient = await getPushMessageApiClient(); + return !messageId ? apiClient.create(message) : apiClient.update(message); }, - remove: () => { - throw new Error("Function not implemented."); + remove: async ({ id }) => { + if (id) { + return (await getPushMessageApiClient()).delete([id]); + } }, }); @@ -43,9 +49,12 @@ export default (args: { const scope = ref({ toolbarOverrides: { saveChanges: { - isVisible: computed(() => !args.props.param), + isVisible: computed(() => isEditable()), disabled: computed(() => validationState.value.disabled), }, + remove: { + isVisible: computed(() => messageId != null && isEditable()), + }, }, loadMembers: async (keyword?: string, skip?: number, ids?: string[]) => { return (await getCustomerApiClient()).searchMember({ @@ -58,11 +67,14 @@ export default (args: { take: ids?.length ?? 20, } as MembersSearchCriteria); }, - isReadOnly: () => { - return args.props.param; - }, + isReadOnly: () => !isEditable(), }); + function isEditable(): boolean { + const message = item.value; + return !messageId || (message != null && message.status !== "Sent"); + } + const bladeTitle = computed(() => { return "Push message details"; }); @@ -70,7 +82,7 @@ export default (args: { watch( () => args?.mounted.value, async () => { - if (!args.props.param) { + if (!messageId) { item.value = reactive(new PushMessage()); validationState.value.resetModified(item.value, true); } diff --git a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/composables/useMessageList/index.ts b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/composables/useMessageList/index.ts index ec17415..ef84588 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/composables/useMessageList/index.ts +++ b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/composables/useMessageList/index.ts @@ -16,11 +16,18 @@ export default () => { const listFactory = useListFactory({ load: async (query) => { const criteria = { ...(query || {}) } as PushMessageSearchCriteria; + criteria.responseGroup = "None"; return (await getApiClient()).search(criteria); }, + remove: async (_query, customQuery) => { + const ids = customQuery.ids; + if (ids) { + return (await getApiClient()).delete(ids); + } + }, }); - const { load, items, pagination, loading, query } = listFactory({ pageSize: 20 }); + const { load, remove, items, pagination, loading, query } = listFactory({ pageSize: 20 }); const { openBlade, resolveBladeByName } = useBladeNavigation(); async function openDetailsBlade(data?: Omit["0"], "blade">) { @@ -32,14 +39,15 @@ export default () => { const scope = ref({ openDetailsBlade, - deleteItem: () => { - alert("Delete item"); + isReadOnly: (data: { item: PushMessage }) => { + return data.item.status === "Sent"; }, }); return { items, load, + remove, loading, pagination, query, diff --git a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/locales/en.json b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/locales/en.json index 43da652..f2dfe54 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/locales/en.json +++ b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/locales/en.json @@ -18,8 +18,9 @@ "TOTALS": "Count:", "HEADER": { "ID": "ID", - "CREATED_DATE": "Date", - "CREATED_BY": "Author", + "CREATED_DATE": "Created date", + "CREATED_BY": "Created by", + "STATUS": "Status", "SHORT_MESSAGE": "Message" }, "EMPTY": { @@ -39,11 +40,30 @@ }, "EMPTY": { "NO_ITEMS": "There are no items yet" + }, + "DELETE_SELECTED_CONFIRMATION": { + "MESSAGE": "Are you sure you want to delete {count} selected messages?" } }, "DETAILS": { "TOOLBAR": { - "SAVE": "Save" + "SAVE": "Save", + "DELETE": "Delete" + }, + "FORM": { + "MESSAGE": { + "LABEL": "Message" + }, + "RECIPIENTS": { + "LABEL": "Recipients", + "PLACEHOLDER": "Select recipients" + }, + "TOPIC": { + "LABEL": "Topic" + }, + "START_DATE": { + "LABEL": "Start date" + } }, "WIDGETS": { "RECIPIENTS": "Status" @@ -64,7 +84,8 @@ } }, "ALERTS": { - "CLOSE_CONFIRMATION": "You have unsaved changes\nClose anyway?" + "CLOSE_CONFIRMATION": "You have unsaved changes\nClose anyway?", + "DELETE": "Are you sure you want to delete this message?" } } } diff --git a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/pages/messageDetails.ts b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/pages/messageDetails.ts index 6af5bba..91af30a 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/pages/messageDetails.ts +++ b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/pages/messageDetails.ts @@ -14,6 +14,12 @@ export const details: DynamicDetailsSchema = { title: "PUSH_MESSAGES.PAGES.DETAILS.TOOLBAR.SAVE", method: "saveChanges", }, + { + id: "delete", + icon: "fas fa-trash", + title: "PUSH_MESSAGES.PAGES.DETAILS.TOOLBAR.DELETE", + method: "remove", + }, ], }, content: [ @@ -24,7 +30,7 @@ export const details: DynamicDetailsSchema = { { id: "shortMessage", component: "vc-editor", - label: "Message", + label: "PUSH_MESSAGES.PAGES.DETAILS.FORM.MESSAGE.LABEL", property: "shortMessage", rules: { required: true }, disabled: { method: "isReadOnly" }, @@ -35,15 +41,33 @@ export const details: DynamicDetailsSchema = { searchable: true, emitValue: true, multiple: true, - label: "Recipients", + label: "PUSH_MESSAGES.PAGES.DETAILS.FORM.RECIPIENTS.LABEL", property: "memberIds", - placeholder: "Select recipients", + placeholder: "PUSH_MESSAGES.PAGES.DETAILS.FORM.RECIPIENTS.PLACEHOLDER", optionValue: "id", optionLabel: "name", optionsMethod: "loadMembers", rules: { required: true }, disabled: { method: "isReadOnly" }, }, + { + id: "topic", + component: "vc-input", + variant: "text", + property: "topic", + label: "PUSH_MESSAGES.PAGES.DETAILS.FORM.TOPIC.LABEL", + rules: { max: 128 }, + disabled: { method: "isReadOnly" }, + }, + { + id: "startDate", + component: "vc-input", + variant: "datetime-local", + property: "startDate", + label: "PUSH_MESSAGES.PAGES.DETAILS.FORM.START_DATE.LABEL", + rules: {}, + disabled: { method: "isReadOnly" }, + }, ], }, { diff --git a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/pages/messageList.ts b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/pages/messageList.ts index 1ad7f21..f953969 100644 --- a/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/pages/messageList.ts +++ b/src/VirtoCommerce.PushMessages.Web/App/src/modules/push-messages/pages/messageList.ts @@ -23,6 +23,12 @@ export const grid: DynamicGridSchema = { title: "PUSH_MESSAGES.PAGES.LIST.TOOLBAR.NEW", method: "openAddBlade", }, + { + id: "deleteSelected", + icon: "fas fa-trash", + title: "PUSH_MESSAGES.PAGES.LIST.TOOLBAR.DELETE", + method: "removeItems", + }, ], menuItem: { title: "PUSH_MESSAGES.MENU.TITLE", @@ -34,12 +40,23 @@ export const grid: DynamicGridSchema = { { id: "itemsGrid", component: "vc-table", + multiselect: true, + actions: [ + { + id: "deleteAction", + icon: "fas fa-trash", + title: "PUSH_MESSAGES.PAGES.LIST.TABLE.ACTIONS.DELETE", + type: "danger", + position: "left", + method: "removeItems", + disabled: { method: "isReadOnly" }, + }, + ], columns: [ { id: "id", title: "PUSH_MESSAGES.PAGES.LIST.TABLE.HEADER.ID", sortable: true, - width: "21em", visible: false, }, { @@ -47,16 +64,19 @@ export const grid: DynamicGridSchema = { title: "PUSH_MESSAGES.PAGES.LIST.TABLE.HEADER.CREATED_DATE", sortable: true, type: "date-time", - width: "14em", alwaysVisible: true, }, { id: "createdBy", title: "PUSH_MESSAGES.PAGES.LIST.TABLE.HEADER.CREATED_BY", sortable: true, - width: "14em", visible: false, }, + { + id: "status", + title: "PUSH_MESSAGES.PAGES.LIST.TABLE.HEADER.STATUS", + sortable: true, + }, { id: "shortMessage", title: "PUSH_MESSAGES.PAGES.LIST.TABLE.HEADER.SHORT_MESSAGE", diff --git a/src/VirtoCommerce.PushMessages.Web/Controllers/Api/PushMessageController.cs b/src/VirtoCommerce.PushMessages.Web/Controllers/Api/PushMessageController.cs index 0aec37b..23f502e 100644 --- a/src/VirtoCommerce.PushMessages.Web/Controllers/Api/PushMessageController.cs +++ b/src/VirtoCommerce.PushMessages.Web/Controllers/Api/PushMessageController.cs @@ -46,9 +46,16 @@ public async Task> Search([FromBody] PushM [HttpPost] [Authorize(ModuleConstants.Security.Permissions.Create)] - public async Task> Create([FromBody] PushMessage model) + public Task> Create([FromBody] PushMessage model) { model.Id = null; + return Update(model); + } + + [HttpPut] + [Authorize(ModuleConstants.Security.Permissions.Update)] + public async Task> Update([FromBody] PushMessage model) + { await _messageService.SaveChangesAsync([model]); return Ok(model); } diff --git a/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json b/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json index 45ed69f..f43b455 100644 --- a/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json +++ b/src/VirtoCommerce.PushMessages.Web/Localizations/en.PushMessages.json @@ -1,11 +1,4 @@ { - "PushMessages": { - "blades": { - "hello-world": { - "title": "Hello world blade" - } - } - }, "permissions": { "PushMessages:access": "Open PushMessages menu", "PushMessages:create": "Create PushMessages related data", @@ -14,15 +7,17 @@ "PushMessages:delete": "Delete PushMessages related data" }, "settings": { - "PushMessages": { - "PushMessagesEnabled": { - "title": "PushMessages Enabled", - "description": "PushMessages Enabled setting" - }, - "PushMessagesPassword": { - "title": "PushMessages Password", - "description": "PushMessages Password setting" - } + "PushMessages.BatchSize": { + "title": "Batch size", + "description": "" + }, + "PushMessages.BackgroundJobs.Enable": { + "title": "Background jobs enable", + "description": "" + }, + "PushMessages.BackgroundJobs.CronExpression": { + "title": "Background jobs cron expression", + "description": "" } } } diff --git a/src/VirtoCommerce.PushMessages.Web/Module.cs b/src/VirtoCommerce.PushMessages.Web/Module.cs index c3a25c3..10b5f0a 100644 --- a/src/VirtoCommerce.PushMessages.Web/Module.cs +++ b/src/VirtoCommerce.PushMessages.Web/Module.cs @@ -12,9 +12,13 @@ using VirtoCommerce.Platform.Core.Modularity; using VirtoCommerce.Platform.Core.Security; using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.Platform.Core.Settings.Events; using VirtoCommerce.PushMessages.Core; +using VirtoCommerce.PushMessages.Core.BackgroundJobs; using VirtoCommerce.PushMessages.Core.Events; using VirtoCommerce.PushMessages.Core.Services; +using VirtoCommerce.PushMessages.Data.BackgroundJobs; +using VirtoCommerce.PushMessages.Data.Handlers; using VirtoCommerce.PushMessages.Data.MySql; using VirtoCommerce.PushMessages.Data.PostgreSql; using VirtoCommerce.PushMessages.Data.Repositories; @@ -62,6 +66,9 @@ public void Initialize(IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + // GraphQL var assemblyMarker = typeof(AssemblyMarker); var graphQlBuilder = new CustomGraphQLBuilder(serviceCollection); @@ -84,14 +91,20 @@ public void PostInitialize(IApplicationBuilder appBuilder) // Register permissions var permissionsRegistrar = serviceProvider.GetRequiredService(); - permissionsRegistrar.RegisterPermissions(ModuleInfo.Id, "PushMessages", ModuleConstants.Security.Permissions.AllPermissions); + permissionsRegistrar.RegisterPermissions(ModuleInfo.Id, "Push Messages", ModuleConstants.Security.Permissions.AllPermissions); // Apply migrations using var serviceScope = serviceProvider.CreateScope(); using var dbContext = serviceScope.ServiceProvider.GetRequiredService(); dbContext.Database.Migrate(); + // Register event handlers + appBuilder.RegisterEventHandler(); appBuilder.RegisterEventHandler(); + + // Schedule background jobs + var pushMessageJobService = serviceProvider.GetService(); + pushMessageJobService.StartStopRecurringJobs().GetAwaiter().GetResult(); } public void Uninstall()