From 9d5ec01d57255ffe03d906d338a565d8afe0c954 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 11 Jan 2024 18:27:33 +0100 Subject: [PATCH] Notifications for jobs. (#1063) --- backend/i18n/source/backend_en.json | 14 ++-- .../Backup/BackupJob.cs | 7 +- .../Backup/RestoreJob.cs | 2 +- .../CommentCollaborationHandler.cs | 51 ++++++++++---- .../History/HistoryService.cs | 5 ++ .../History/NotifoService.cs | 58 ++++++++++------ .../Jobs/JobProcessor.cs | 28 ++++++++ .../Jobs/JobRequest.cs | 2 + .../Rules/Runner/DefaultRuleRunnerService.cs | 7 +- .../Rules/Runner/IRuleRunnerService.cs | 3 +- .../Rules/Runner/RuleRunnerJob.cs | 16 +++-- .../Comments/CommentCreated.cs | 2 + backend/src/Squidex.Shared/Texts.fr.resx | 36 +++++----- backend/src/Squidex.Shared/Texts.it.resx | 36 +++++----- backend/src/Squidex.Shared/Texts.nl.resx | 36 +++++----- backend/src/Squidex.Shared/Texts.pt.resx | 36 +++++----- backend/src/Squidex.Shared/Texts.resx | 36 +++++----- backend/src/Squidex.Shared/Texts.zh.resx | 36 +++++----- .../Api/Controllers/Rules/RulesController.cs | 2 +- .../CommentCollaborationHandlerTests.cs | 66 +++++++++++++++++++ .../notifications-menu.component.html | 2 +- .../internal/notifications-menu.component.ts | 2 +- 22 files changed, 339 insertions(+), 144 deletions(-) diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 95dcd4aae..77605be36 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -262,15 +262,17 @@ "history.teams.planChanged": "changed plan to {[Plan]}.", "history.teams.planReset": "resetted plan.", "history.teams.updated": "updated general settings and renamed name to {[Name]}.", - "job.backup": "Backup", - "job.restore": "Restore", - "job.ruleRun": "Replay Rule.", - "job.ruleRunNamed": "Replay Rule '{name}'.", - "job.ruleRunNamedSnapshot": "Replay Rule '{name}' from states.", - "job.ruleRunSnapshot": "Replay Rule from states", "jobs.alreadyRunning": "Another job is already running.", + "jobs.backup": "Backup", "jobs.invalidTaskName": "Invalid task name", "jobs.maxReached": "You cannot have more than {max} backups.", + "jobs.notifyFailed": "Your job '{job}' failed to completed.", + "jobs.notifySuccess": "Your job '{job}' has been completed successfully.", + "jobs.restore": "Restore", + "jobs.ruleRun": "Replay Rule.", + "jobs.ruleRunNamed": "Replay Rule '{name}'.", + "jobs.ruleRunNamedSnapshot": "Replay Rule '{name}' from states.", + "jobs.ruleRunSnapshot": "Replay Rule from states", "login.githubPrivateEmail": "Your email address is set to private in Github. Please set it to public to use Github login.", "schemas.dateTimeCalculatedDefaultAndDefaultError": "Calculated default value and default value cannot be used together.", "schemas.duplicateFieldName": "Field '{field}' has been added twice.", diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupJob.cs index 85e78fabe..6c6930863 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupJob.cs @@ -57,7 +57,10 @@ public sealed class BackupJob : IJobRunner { [ArgAppId] = app.Id.ToString(), [ArgAppName] = app.Name - }); + }) with + { + AppId = app.NamedId() + }; } public Task DownloadAsync(Job state, Stream stream, @@ -81,7 +84,7 @@ public sealed class BackupJob : IJobRunner context.Job.File = new JobFile($"backup-{appName}-{context.Job.Started:yyyy-MM-dd_HH-mm-ss}.zip", "application/zip"); // Use a readable name to describe the job. - context.Job.Description = T.Get("job.backup"); + context.Job.Description = T.Get("jobs.backup"); var handlers = backupHandlerFactory.CreateMany(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs index 32538d0c6..497e00562 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs @@ -107,7 +107,7 @@ public sealed class RestoreJob : IJobRunner }; // Use a readable name to describe the job. - context.Job.Description = T.Get("job.restore"); + context.Job.Description = T.Get("jobs.restore"); try { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs index dc8659ff0..26352b53b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs @@ -96,7 +96,7 @@ public sealed partial class CommentCollaborationHandler : IDocumentCallback, ICo public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) { - if (!IsResourceDocument(@event.Context.DocumentName, out var appId, out var resourceId)) + if (!IsResourceOrUserDocument(@event.Context.DocumentName, out var appId, out var resourceId)) { return default; } @@ -239,26 +239,55 @@ public sealed partial class CommentCollaborationHandler : IDocumentCallback, ICo return $"apps/{appId}/{resourceId}"; } - private static bool IsResourceDocument(string name, out NamedId appId, out DomainId resourceId) + private static bool IsResourceOrUserDocument(string name, out NamedId appId, out DomainId resourceId) { resourceId = default; - if (!name.StartsWith("apps", StringComparison.Ordinal)) + // Result can be null, if the method returns null. + appId = default!; + + static bool IsResourceDocument(string name, ref NamedId appId, ref DomainId resourceId) { - appId = default!; - return false; + var parts = name.Split('/'); + + if (parts.Length < 3 || !NamedId.TryParse(parts[1], DomainId.TryParse, out appId!)) + { + return false; + } + + // Assume tha tour ID could also have slashes. + resourceId = DomainId.Create(string.Join('/', parts.Skip(2))); + return true; } - var parts = name.Split('/'); + static bool IsUserDocument(string name, ref NamedId appId, ref DomainId resourceId) + { + var parts = name.Split('/'); + + if (parts.Length < 2) + { + return false; + } + + // Use a dummy value for the app ID. + appId = CommentCreated.NoApp; + + // Assume tha tour ID could also have slashes. + resourceId = DomainId.Create(string.Join('/', parts.Skip(1))); + return true; + } + + if (name.StartsWith("apps/", StringComparison.Ordinal)) + { + return IsResourceDocument(name, ref appId, ref resourceId); + } - if (parts.Length < 3 || !NamedId.TryParse(parts[1], DomainId.TryParse, out appId!)) + if (name.StartsWith("users/", StringComparison.Ordinal)) { - appId = default!; - return false; + return IsUserDocument(name, ref appId, ref resourceId); } - resourceId = DomainId.Create(string.Join('/', parts.Skip(2))); - return true; + return false; } [GeneratedRegex(@"@(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*", RegexOptions.Compiled | RegexOptions.ExplicitCapture, matchTimeoutMilliseconds: 100)] diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs index 98267a7be..c61010a6a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs @@ -7,6 +7,7 @@ using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Comments; using Squidex.Domain.Apps.Events.Teams; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; @@ -64,6 +65,10 @@ public sealed class HistoryService : IHistoryService, IEventConsumer { switch (@event.Payload) { + case CommentCreated: + targets.Add((@event, null)); + break; + case AppEvent appEvent: { var historyEvent = await CreateEvent(appEvent.AppId.Id, appEvent.Actor, @event); diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs index 46012281c..c6f056f9b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs @@ -237,13 +237,17 @@ public class NotifoService : IUserEvents private IEnumerable CreateRequests(Envelope @event, HistoryEvent? historyEvent) { - if (@event.Payload is CommentCreated { Mentions.Length: > 0 } comment) + if (@event.Payload is CommentCreated { Mentions.Length: > 0, AppId: not null } comment) { foreach (var userId in comment.Mentions) { yield return CreateMentionRequest(comment, userId); } } + else if (@event.Payload is CommentCreated notification && notification.AppId == CommentCreated.NoApp) + { + yield return CreateMentionRequest(notification, notification.CommentsId.ToString()); + } else if (historyEvent != null && @event.Payload is AppEvent appEvent) { yield return CreateHistoryRequest(historyEvent, appEvent); @@ -262,25 +266,17 @@ public class NotifoService : IUserEvents publishRequest.Properties.Add(key, value); } - if (payload is AppEvent appEvent) - { - publishRequest.Properties["SquidexApp"] = appEvent.AppId.Name; - } - if (payload is SquidexEvent squidexEvent) { SetUser(squidexEvent, publishRequest); } - if (payload is AppEvent appEvent2) + if (payload is AppEvent appEvent) { - publishRequest.Topic = BuildTopic(GetAppPrefix(appEvent2), historyEvent); + publishRequest.Properties["SquidexApp"] = appEvent.AppId.Name; } - if (payload is TeamEvent teamEvent) - { - publishRequest.Topic = BuildTopic(GetTeamPrefix(teamEvent), historyEvent); - } + SetTopic(historyEvent, payload, publishRequest); if (payload is ContentEvent @event and not ContentDeleted) { @@ -294,26 +290,46 @@ public class NotifoService : IUserEvents return publishRequest; } + private static void SetTopic(HistoryEvent historyEvent, IEvent payload, PublishDto publishRequest) + { + if (payload is AppEvent appEvent) + { + publishRequest.Topic = BuildTopic(GetAppPrefix(appEvent), historyEvent); + } + else if (payload is TeamEvent teamEvent) + { + publishRequest.Topic = BuildTopic(GetTeamPrefix(teamEvent), historyEvent); + } + } + private static PublishDto CreateMentionRequest(CommentCreated comment, string userId) { var publishRequest = new PublishDto { - Topic = $"users/{userId}" + Topic = $"users/{userId}", + Preformatted = new NotificationFormattingDto + { + Subject = + { + ["en"] = comment.Text + } + } }; - publishRequest.Properties["SquidexApp"] = comment.AppId.Name; - - publishRequest.Preformatted = new NotificationFormattingDto + if (comment.AppId != null) { - Subject = + publishRequest.Properties = new NotificationProperties { - ["en"] = comment.Text - } - }; + ["SquidexApp"] = comment.AppId.Name + }; + } if (comment.Url?.IsAbsoluteUri == true) { - publishRequest.Preformatted.LinkUrl["en"] = comment.Url.ToString(); + publishRequest.Preformatted.LinkUrl = new LocalizedText() + { + ["en"] = comment.Url.ToString() + }; } SetUser(comment, publishRequest); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobProcessor.cs b/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobProcessor.cs index 291632e30..3ee2537a3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobProcessor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobProcessor.cs @@ -8,6 +8,8 @@ using Microsoft.Extensions.Logging; using NodaTime; using Squidex.Caching; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Entities.Collaboration; using Squidex.Infrastructure; using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; @@ -20,6 +22,8 @@ public sealed class JobProcessor private readonly DomainId ownerId; private readonly IEnumerable runners; private readonly ILocalCache localCache; + private readonly ICollaborationService collaboration; + private readonly IUrlGenerator urlGenerator; private readonly ILogger log; private readonly SimpleState state; private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1); @@ -30,12 +34,16 @@ public sealed class JobProcessor public JobProcessor(DomainId ownerId, IEnumerable runners, ILocalCache localCache, + ICollaborationService collaboration, IPersistenceFactory persistenceFactory, + IUrlGenerator urlGenerator, ILogger log) { this.ownerId = ownerId; this.runners = runners; this.localCache = localCache; + this.collaboration = collaboration; + this.urlGenerator = urlGenerator; this.log = log; state = new SimpleState(persistenceFactory, GetType(), ownerId); @@ -148,6 +156,13 @@ public sealed class JobProcessor try { await ProcessAsync(context, runner, context.CancellationToken); + + await NotifyAsync(request, context, "jobs.notifySuccess"); + } + catch + { + await NotifyAsync(request, context, "jobs.notifyFailed"); + throw; } finally { @@ -158,6 +173,19 @@ public sealed class JobProcessor }, ct); } + private async Task NotifyAsync(JobRequest request, JobRunContext context, string text) + { + if (request.AppId == null || request.Actor.IsClient) + { + return; + } + + var notificationText = T.Get(text, new { job = context.Job.Description }); + var notificationUrl = new Uri(urlGenerator.JobsUI(request.AppId)); + + await collaboration.NotifyAsync(request.Actor.Identifier, notificationText, request.Actor, notificationUrl, false, default); + } + private async Task ProcessAsync(JobRunContext context, IJobRunner runner, CancellationToken ct) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobRequest.cs b/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobRequest.cs index 9a71445a4..8c1882fa9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobRequest.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobRequest.cs @@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Jobs; public record struct JobRequest(RefToken Actor, string TaskName, ReadonlyDictionary Arguments) { + public NamedId? AppId { get; set; } + public static JobRequest Create(RefToken actor, string taskName, Dictionary? arguments = null) { var args = arguments?.ToReadonlyDictionary() ?? ReadonlyDictionary.Empty(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs index ffdd0039b..db539449a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs @@ -6,6 +6,7 @@ // ========================================================================== using NodaTime; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; @@ -121,12 +122,12 @@ public sealed class DefaultRuleRunnerService : IRuleRunnerService return jobService.CancelAsync(appId, taskName, ct); } - public Task RunAsync(RefToken actor, DomainId appId, DomainId ruleId, bool fromSnapshots = false, + public Task RunAsync(RefToken actor, App app, DomainId ruleId, bool fromSnapshots = false, CancellationToken ct = default) { - var job = RuleRunnerJob.BuildRequest(actor, ruleId, fromSnapshots); + var job = RuleRunnerJob.BuildRequest(actor, app, ruleId, fromSnapshots); - return jobService.StartAsync(appId, job, ct); + return jobService.StartAsync(app.Id, job, ct); } public async Task GetRunningRuleIdAsync(DomainId appId, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs index 375653521..0aec49f7e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; @@ -18,7 +19,7 @@ public interface IRuleRunnerService Task> SimulateAsync(Rule rule, CancellationToken ct = default); - Task RunAsync(RefToken actor, DomainId appId, DomainId ruleId, bool fromSnapshots = false, + Task RunAsync(RefToken actor, App app, DomainId ruleId, bool fromSnapshots = false, CancellationToken ct = default); Task CancelAsync(DomainId appId, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerJob.cs index e8f0be5c1..9034756f1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerJob.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.Extensions.Logging; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Jobs; @@ -66,7 +67,7 @@ public sealed class RuleRunnerJob : IJobRunner return DomainId.Create(ruleId); } - public static JobRequest BuildRequest(RefToken actor, DomainId ruleId, bool snapshot) + public static JobRequest BuildRequest(RefToken actor, App app, DomainId ruleId, bool snapshot) { return JobRequest.Create( actor, @@ -75,7 +76,10 @@ public sealed class RuleRunnerJob : IJobRunner { [ArgRuleId] = ruleId.ToString(), [ArgSnapshot] = snapshot.ToString() - }); + }) with + { + AppId = app.NamedId() + }; } public async Task RunAsync(JobRunContext context, @@ -118,16 +122,16 @@ public sealed class RuleRunnerJob : IJobRunner if (!string.IsNullOrWhiteSpace(rule.Name)) { var key = fromSnapshot ? - "job.ruleRunNamedSnapshot" : - "job.ruleRunName"; + "jobs.ruleRunNamedSnapshot" : + "jobs.ruleRunName"; run.Job.Description = T.Get(key, new { name = rule.Name }); } else { var key = fromSnapshot ? - "job.ruleRunSnapshot" : - "job.ruleRun"; + "jobs.ruleRunSnapshot" : + "jobs.ruleRun"; run.Job.Description = T.Get(key); } diff --git a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs index c44590112..bb7962cf1 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs @@ -13,6 +13,8 @@ namespace Squidex.Domain.Apps.Events.Comments; [EventType(nameof(CommentCreated))] public sealed class CommentCreated : AppEvent { + public static readonly NamedId NoApp = NamedId.Of(DomainId.NewGuid(), "no-app"); + public DomainId CommentsId { get; set; } public DomainId CommentId { get; set; } diff --git a/backend/src/Squidex.Shared/Texts.fr.resx b/backend/src/Squidex.Shared/Texts.fr.resx index bf504ee17..3f7d6222b 100644 --- a/backend/src/Squidex.Shared/Texts.fr.resx +++ b/backend/src/Squidex.Shared/Texts.fr.resx @@ -871,33 +871,39 @@ paramètres généraux mis à jour et nom renommé en {[Name]}. - + + Another job is already running. + + Backup - + + Invalid task name + + + You cannot have more than {max} backups. + + + Your job '{job}' failed to completed. + + + Your job '{job}' has been completed successfully. + + Restore - + Replay Rule. - + Replay Rule '{name}'. - + Replay Rule '{name}' from states. - + Replay Rule from states - - Another job is already running. - - - Invalid task name - - - You cannot have more than {max} backups. - Votre adresse e-mail est définie sur privé dans Github. Veuillez le définir sur public pour utiliser la connexion Github. diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index 4594f4a80..3b927df2a 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -871,33 +871,39 @@ updated general settings and renamed name to {[Name]}. - + + Another job is already running. + + Backup - + + Invalid task name + + + You cannot have more than {max} backups. + + + Your job '{job}' failed to completed. + + + Your job '{job}' has been completed successfully. + + Restore - + Replay Rule. - + Replay Rule '{name}'. - + Replay Rule '{name}' from states. - + Replay Rule from states - - Another job is already running. - - - Invalid task name - - - You cannot have more than {max} backups. - Il tuo indirizzo email è impostato su privato in Github. Impostalo come pubblico per poter utilizzare il login Github. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index 3a17daefc..de182721a 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -871,33 +871,39 @@ updated general settings and renamed name to {[Name]}. - + + Another job is already running. + + Backup - + + Invalid task name + + + You cannot have more than {max} backups. + + + Your job '{job}' failed to completed. + + + Your job '{job}' has been completed successfully. + + Restore - + Replay Rule. - + Replay Rule '{name}'. - + Replay Rule '{name}' from states. - + Replay Rule from states - - Another job is already running. - - - Invalid task name - - - You cannot have more than {max} backups. - Jouw e-mailadres is ingesteld op privé in Github. Stel het in op openbaar om Github-login te gebruiken. diff --git a/backend/src/Squidex.Shared/Texts.pt.resx b/backend/src/Squidex.Shared/Texts.pt.resx index 83f457825..50ad42ba8 100644 --- a/backend/src/Squidex.Shared/Texts.pt.resx +++ b/backend/src/Squidex.Shared/Texts.pt.resx @@ -871,33 +871,39 @@ actualizadas configurações gerais e renomeado para {[Name]}. - + + Another job is already running. + + Backup - + + Invalid task name + + + You cannot have more than {max} backups. + + + Your job '{job}' failed to completed. + + + Your job '{job}' has been completed successfully. + + Restore - + Replay Rule. - + Replay Rule '{name}'. - + Replay Rule '{name}' from states. - + Replay Rule from states - - Another job is already running. - - - Invalid task name - - - You cannot have more than {max} backups. - O seu Email é privado no Github. Altere para publico no Github e tente novamente. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index 61b3dfe96..4197bb4ec 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -871,33 +871,39 @@ updated general settings and renamed name to {[Name]}. - + + Another job is already running. + + Backup - + + Invalid task name + + + You cannot have more than {max} backups. + + + Your job '{job}' failed to completed. + + + Your job '{job}' has been completed successfully. + + Restore - + Replay Rule. - + Replay Rule '{name}'. - + Replay Rule '{name}' from states. - + Replay Rule from states - - Another job is already running. - - - Invalid task name - - - You cannot have more than {max} backups. - Your email address is set to private in Github. Please set it to public to use Github login. diff --git a/backend/src/Squidex.Shared/Texts.zh.resx b/backend/src/Squidex.Shared/Texts.zh.resx index 916f06cb5..6f519aef8 100644 --- a/backend/src/Squidex.Shared/Texts.zh.resx +++ b/backend/src/Squidex.Shared/Texts.zh.resx @@ -871,33 +871,39 @@ updated general settings and renamed name to {[Name]}. - + + Another job is already running. + + Backup - + + Invalid task name + + + You cannot have more than {max} backups. + + + Your job '{job}' failed to completed. + + + Your job '{job}' has been completed successfully. + + Restore - + Replay Rule. - + Replay Rule '{name}'. - + Replay Rule '{name}' from states. - + Replay Rule from states - - Another job is already running. - - - Invalid task name - - - You cannot have more than {max} backups. - 您的邮箱在 Github 中设置为私有。请设置为公开以使用 Github 登录。 diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 53ef4bd37..30916a097 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -241,7 +241,7 @@ public sealed class RulesController : ApiController [ApiCosts(1)] public async Task PutRuleRun(string app, DomainId id, [FromQuery] bool fromSnapshots = false) { - await ruleRunnerService.RunAsync(User.Token()!, App.Id, id, fromSnapshots, HttpContext.RequestAborted); + await ruleRunnerService.RunAsync(User.Token()!, App, id, fromSnapshots, HttpContext.RequestAborted); return NoContent(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentCollaborationHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentCollaborationHandlerTests.cs index 8157d9820..3129a0c9c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentCollaborationHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentCollaborationHandlerTests.cs @@ -136,6 +136,28 @@ public class CommentCollaborationHandlerTests : GivenContext }, opts => opts.Excluding(x => x.CommentId)); } + [Fact] + public async Task Should_publish_event_for_notification() + { + var text = "My Comment"; + + var commentsId = DomainId.Create("user42"); + var commentItem = new Comment(clock.GetCurrentInstant(), User, text); + + var storedEvent = await CreateNotificationAsync(commentsId, commentItem); + + storedEvent?.Payload.Should().BeEquivalentTo( + new CommentCreated + { + Actor = User, + AppId = CommentCreated.NoApp, + CommentId = default, + CommentsId = commentsId, + Text = commentItem.Text, + Mentions = null, + }, opts => opts.Excluding(x => x.CommentId)); + } + [Fact] public async Task Should_not_enrich_comment_with_mentioned_users_if_users_not_found() { @@ -252,6 +274,50 @@ public class CommentCollaborationHandlerTests : GivenContext return storedEvent; } + private async Task?> CreateNotificationAsync(DomainId commentsId, Comment comment) + { + var document = new Doc(); + var docName = sut.UserDocument(commentsId.ToString()); + + documentManager.Doc = document; + + var stream = document.Array("stream"); + + await sut.OnDocumentLoadedAsync(new DocumentLoadEvent + { + Context = new DocumentContext(docName, 0), + Document = document, + Source = documentManager, + }); + + var commentJson = TestUtils.DefaultSerializer.Serialize(comment); + + Envelope? storedEvent = null; + + A.CallTo(() => eventFormatter.ToEventData(A>._, A._, true)) + .Invokes(c => + { + storedEvent = c.GetArgument>(0); + }); + + await documentManager.UpdateDocAsync(null!, doc => + { + using (var transaction = doc.WriteTransaction()) + { + stream.InsertRange(transaction, 0, InputFactory.FromJson(commentJson)); + } + }, default); + + await sut.LastTask; + + var streamName = $"comments-{DomainId.Combine(CommentCreated.NoApp, commentsId)}"; + + A.CallTo(() => eventStore.AppendAsync(A._, streamName, EtagVersion.Any, A>._, A._)) + .MustHaveHappened(); + + return storedEvent; + } + private void SetupUser(string id, string email) { var user = UserMocks.User(id, email); diff --git a/frontend/src/app/shell/pages/internal/notifications-menu.component.html b/frontend/src/app/shell/pages/internal/notifications-menu.component.html index 439e1f4c8..29f362b31 100644 --- a/frontend/src/app/shell/pages/internal/notifications-menu.component.html +++ b/frontend/src/app/shell/pages/internal/notifications-menu.component.html @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/frontend/src/app/shell/pages/internal/notifications-menu.component.ts b/frontend/src/app/shell/pages/internal/notifications-menu.component.ts index 490f4142f..80dbeeb60 100644 --- a/frontend/src/app/shell/pages/internal/notifications-menu.component.ts +++ b/frontend/src/app/shell/pages/internal/notifications-menu.component.ts @@ -28,7 +28,7 @@ export class NotificationsMenuComponent { constructor(authService: AuthService, uiOptions: UIOptions, ) { const notifoApiKey = authService.user?.notifoToken; - const notifoApiUrl = uiOptions.value.notifoAPi; + const notifoApiUrl = uiOptions.value.notifoApi; this.isNotifoConfigured = !!notifoApiKey && !!notifoApiUrl; }