Browse Source

Notifications for jobs. (#1063)

pull/1065/head
Sebastian Stehle 2 years ago
committed by GitHub
parent
commit
9d5ec01d57
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      backend/i18n/source/backend_en.json
  2. 7
      backend/src/Squidex.Domain.Apps.Entities/Backup/BackupJob.cs
  3. 2
      backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs
  4. 51
      backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs
  5. 5
      backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs
  6. 58
      backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs
  7. 28
      backend/src/Squidex.Domain.Apps.Entities/Jobs/JobProcessor.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Entities/Jobs/JobRequest.cs
  9. 7
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs
  10. 3
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs
  11. 16
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerJob.cs
  12. 2
      backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs
  13. 36
      backend/src/Squidex.Shared/Texts.fr.resx
  14. 36
      backend/src/Squidex.Shared/Texts.it.resx
  15. 36
      backend/src/Squidex.Shared/Texts.nl.resx
  16. 36
      backend/src/Squidex.Shared/Texts.pt.resx
  17. 36
      backend/src/Squidex.Shared/Texts.resx
  18. 36
      backend/src/Squidex.Shared/Texts.zh.resx
  19. 2
      backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  20. 66
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentCollaborationHandlerTests.cs
  21. 2
      frontend/src/app/shell/pages/internal/notifications-menu.component.html
  22. 2
      frontend/src/app/shell/pages/internal/notifications-menu.component.ts

14
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.",

7
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();

2
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
{

51
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<DomainId> appId, out DomainId resourceId)
private static bool IsResourceOrUserDocument(string name, out NamedId<DomainId> 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<DomainId> appId, ref DomainId resourceId)
{
appId = default!;
return false;
var parts = name.Split('/');
if (parts.Length < 3 || !NamedId<DomainId>.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<DomainId> 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<DomainId>.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)]

5
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);

58
backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs

@ -237,13 +237,17 @@ public class NotifoService : IUserEvents
private IEnumerable<PublishDto> CreateRequests(Envelope<IEvent> @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);

28
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<IJobRunner> runners;
private readonly ILocalCache localCache;
private readonly ICollaborationService collaboration;
private readonly IUrlGenerator urlGenerator;
private readonly ILogger<JobProcessor> log;
private readonly SimpleState<JobsState> state;
private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1);
@ -30,12 +34,16 @@ public sealed class JobProcessor
public JobProcessor(DomainId ownerId,
IEnumerable<IJobRunner> runners,
ILocalCache localCache,
ICollaborationService collaboration,
IPersistenceFactory<JobsState> persistenceFactory,
IUrlGenerator urlGenerator,
ILogger<JobProcessor> log)
{
this.ownerId = ownerId;
this.runners = runners;
this.localCache = localCache;
this.collaboration = collaboration;
this.urlGenerator = urlGenerator;
this.log = log;
state = new SimpleState<JobsState>(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)
{

2
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<string, string> Arguments)
{
public NamedId<DomainId>? AppId { get; set; }
public static JobRequest Create(RefToken actor, string taskName, Dictionary<string, string>? arguments = null)
{
var args = arguments?.ToReadonlyDictionary() ?? ReadonlyDictionary.Empty<string, string>();

7
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<DomainId?> GetRunningRuleIdAsync(DomainId appId,

3
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<List<SimulatedRuleEvent>> 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,

16
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);
}

2
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<DomainId> NoApp = NamedId.Of(DomainId.NewGuid(), "no-app");
public DomainId CommentsId { get; set; }
public DomainId CommentId { get; set; }

36
backend/src/Squidex.Shared/Texts.fr.resx

@ -871,33 +871,39 @@
<data name="history.teams.updated" xml:space="preserve">
<value>paramètres généraux mis à jour et nom renommé en {[Name]}.</value>
</data>
<data name="job.backup" xml:space="preserve">
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.backup" xml:space="preserve">
<value>Backup</value>
</data>
<data name="job.restore" xml:space="preserve">
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="jobs.notifyFailed" xml:space="preserve">
<value>Your job '{job}' failed to completed.</value>
</data>
<data name="jobs.notifySuccess" xml:space="preserve">
<value>Your job '{job}' has been completed successfully.</value>
</data>
<data name="jobs.restore" xml:space="preserve">
<value>Restore</value>
</data>
<data name="job.ruleRun" xml:space="preserve">
<data name="jobs.ruleRun" xml:space="preserve">
<value>Replay Rule.</value>
</data>
<data name="job.ruleRunNamed" xml:space="preserve">
<data name="jobs.ruleRunNamed" xml:space="preserve">
<value>Replay Rule '{name}'.</value>
</data>
<data name="job.ruleRunNamedSnapshot" xml:space="preserve">
<data name="jobs.ruleRunNamedSnapshot" xml:space="preserve">
<value>Replay Rule '{name}' from states.</value>
</data>
<data name="job.ruleRunSnapshot" xml:space="preserve">
<data name="jobs.ruleRunSnapshot" xml:space="preserve">
<value>Replay Rule from states</value>
</data>
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>Votre adresse e-mail est définie sur privé dans Github. Veuillez le définir sur public pour utiliser la connexion Github.</value>
</data>

36
backend/src/Squidex.Shared/Texts.it.resx

@ -871,33 +871,39 @@
<data name="history.teams.updated" xml:space="preserve">
<value>updated general settings and renamed name to {[Name]}.</value>
</data>
<data name="job.backup" xml:space="preserve">
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.backup" xml:space="preserve">
<value>Backup</value>
</data>
<data name="job.restore" xml:space="preserve">
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="jobs.notifyFailed" xml:space="preserve">
<value>Your job '{job}' failed to completed.</value>
</data>
<data name="jobs.notifySuccess" xml:space="preserve">
<value>Your job '{job}' has been completed successfully.</value>
</data>
<data name="jobs.restore" xml:space="preserve">
<value>Restore</value>
</data>
<data name="job.ruleRun" xml:space="preserve">
<data name="jobs.ruleRun" xml:space="preserve">
<value>Replay Rule.</value>
</data>
<data name="job.ruleRunNamed" xml:space="preserve">
<data name="jobs.ruleRunNamed" xml:space="preserve">
<value>Replay Rule '{name}'.</value>
</data>
<data name="job.ruleRunNamedSnapshot" xml:space="preserve">
<data name="jobs.ruleRunNamedSnapshot" xml:space="preserve">
<value>Replay Rule '{name}' from states.</value>
</data>
<data name="job.ruleRunSnapshot" xml:space="preserve">
<data name="jobs.ruleRunSnapshot" xml:space="preserve">
<value>Replay Rule from states</value>
</data>
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>Il tuo indirizzo email è impostato su privato in Github. Impostalo come pubblico per poter utilizzare il login Github.</value>
</data>

36
backend/src/Squidex.Shared/Texts.nl.resx

@ -871,33 +871,39 @@
<data name="history.teams.updated" xml:space="preserve">
<value>updated general settings and renamed name to {[Name]}.</value>
</data>
<data name="job.backup" xml:space="preserve">
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.backup" xml:space="preserve">
<value>Backup</value>
</data>
<data name="job.restore" xml:space="preserve">
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="jobs.notifyFailed" xml:space="preserve">
<value>Your job '{job}' failed to completed.</value>
</data>
<data name="jobs.notifySuccess" xml:space="preserve">
<value>Your job '{job}' has been completed successfully.</value>
</data>
<data name="jobs.restore" xml:space="preserve">
<value>Restore</value>
</data>
<data name="job.ruleRun" xml:space="preserve">
<data name="jobs.ruleRun" xml:space="preserve">
<value>Replay Rule.</value>
</data>
<data name="job.ruleRunNamed" xml:space="preserve">
<data name="jobs.ruleRunNamed" xml:space="preserve">
<value>Replay Rule '{name}'.</value>
</data>
<data name="job.ruleRunNamedSnapshot" xml:space="preserve">
<data name="jobs.ruleRunNamedSnapshot" xml:space="preserve">
<value>Replay Rule '{name}' from states.</value>
</data>
<data name="job.ruleRunSnapshot" xml:space="preserve">
<data name="jobs.ruleRunSnapshot" xml:space="preserve">
<value>Replay Rule from states</value>
</data>
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>Jouw e-mailadres is ingesteld op privé in Github. Stel het in op openbaar om Github-login te gebruiken.</value>
</data>

36
backend/src/Squidex.Shared/Texts.pt.resx

@ -871,33 +871,39 @@
<data name="history.teams.updated" xml:space="preserve">
<value>actualizadas configurações gerais e renomeado para {[Name]}.</value>
</data>
<data name="job.backup" xml:space="preserve">
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.backup" xml:space="preserve">
<value>Backup</value>
</data>
<data name="job.restore" xml:space="preserve">
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="jobs.notifyFailed" xml:space="preserve">
<value>Your job '{job}' failed to completed.</value>
</data>
<data name="jobs.notifySuccess" xml:space="preserve">
<value>Your job '{job}' has been completed successfully.</value>
</data>
<data name="jobs.restore" xml:space="preserve">
<value>Restore</value>
</data>
<data name="job.ruleRun" xml:space="preserve">
<data name="jobs.ruleRun" xml:space="preserve">
<value>Replay Rule.</value>
</data>
<data name="job.ruleRunNamed" xml:space="preserve">
<data name="jobs.ruleRunNamed" xml:space="preserve">
<value>Replay Rule '{name}'.</value>
</data>
<data name="job.ruleRunNamedSnapshot" xml:space="preserve">
<data name="jobs.ruleRunNamedSnapshot" xml:space="preserve">
<value>Replay Rule '{name}' from states.</value>
</data>
<data name="job.ruleRunSnapshot" xml:space="preserve">
<data name="jobs.ruleRunSnapshot" xml:space="preserve">
<value>Replay Rule from states</value>
</data>
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>O seu Email é privado no Github. Altere para publico no Github e tente novamente.</value>
</data>

36
backend/src/Squidex.Shared/Texts.resx

@ -871,33 +871,39 @@
<data name="history.teams.updated" xml:space="preserve">
<value>updated general settings and renamed name to {[Name]}.</value>
</data>
<data name="job.backup" xml:space="preserve">
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.backup" xml:space="preserve">
<value>Backup</value>
</data>
<data name="job.restore" xml:space="preserve">
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="jobs.notifyFailed" xml:space="preserve">
<value>Your job '{job}' failed to completed.</value>
</data>
<data name="jobs.notifySuccess" xml:space="preserve">
<value>Your job '{job}' has been completed successfully.</value>
</data>
<data name="jobs.restore" xml:space="preserve">
<value>Restore</value>
</data>
<data name="job.ruleRun" xml:space="preserve">
<data name="jobs.ruleRun" xml:space="preserve">
<value>Replay Rule.</value>
</data>
<data name="job.ruleRunNamed" xml:space="preserve">
<data name="jobs.ruleRunNamed" xml:space="preserve">
<value>Replay Rule '{name}'.</value>
</data>
<data name="job.ruleRunNamedSnapshot" xml:space="preserve">
<data name="jobs.ruleRunNamedSnapshot" xml:space="preserve">
<value>Replay Rule '{name}' from states.</value>
</data>
<data name="job.ruleRunSnapshot" xml:space="preserve">
<data name="jobs.ruleRunSnapshot" xml:space="preserve">
<value>Replay Rule from states</value>
</data>
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>Your email address is set to private in Github. Please set it to public to use Github login.</value>
</data>

36
backend/src/Squidex.Shared/Texts.zh.resx

@ -871,33 +871,39 @@
<data name="history.teams.updated" xml:space="preserve">
<value>updated general settings and renamed name to {[Name]}.</value>
</data>
<data name="job.backup" xml:space="preserve">
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.backup" xml:space="preserve">
<value>Backup</value>
</data>
<data name="job.restore" xml:space="preserve">
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="jobs.notifyFailed" xml:space="preserve">
<value>Your job '{job}' failed to completed.</value>
</data>
<data name="jobs.notifySuccess" xml:space="preserve">
<value>Your job '{job}' has been completed successfully.</value>
</data>
<data name="jobs.restore" xml:space="preserve">
<value>Restore</value>
</data>
<data name="job.ruleRun" xml:space="preserve">
<data name="jobs.ruleRun" xml:space="preserve">
<value>Replay Rule.</value>
</data>
<data name="job.ruleRunNamed" xml:space="preserve">
<data name="jobs.ruleRunNamed" xml:space="preserve">
<value>Replay Rule '{name}'.</value>
</data>
<data name="job.ruleRunNamedSnapshot" xml:space="preserve">
<data name="jobs.ruleRunNamedSnapshot" xml:space="preserve">
<value>Replay Rule '{name}' from states.</value>
</data>
<data name="job.ruleRunSnapshot" xml:space="preserve">
<data name="jobs.ruleRunSnapshot" xml:space="preserve">
<value>Replay Rule from states</value>
</data>
<data name="jobs.alreadyRunning" xml:space="preserve">
<value>Another job is already running.</value>
</data>
<data name="jobs.invalidTaskName" xml:space="preserve">
<value>Invalid task name</value>
</data>
<data name="jobs.maxReached" xml:space="preserve">
<value>You cannot have more than {max} backups.</value>
</data>
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>您的邮箱在 Github 中设置为私有。请设置为公开以使用 Github 登录。</value>
</data>

2
backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -241,7 +241,7 @@ public sealed class RulesController : ApiController
[ApiCosts(1)]
public async Task<IActionResult> 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();
}

66
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<Envelope<IEvent>?> 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<IEvent>? storedEvent = null;
A.CallTo(() => eventFormatter.ToEventData(A<Envelope<IEvent>>._, A<Guid>._, true))
.Invokes(c =>
{
storedEvent = c.GetArgument<Envelope<IEvent>>(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<Guid>._, streamName, EtagVersion.Any, A<ICollection<EventData>>._, A<CancellationToken>._))
.MustHaveHappened();
return storedEvent;
}
private void SetupUser(string id, string email)
{
var user = UserMocks.User(id, email);

2
frontend/src/app/shell/pages/internal/notifications-menu.component.html

@ -1,5 +1,5 @@
<ul class="nav navbar-nav align-items-center flex-nowrap">
<sqx-notifo></sqx-notifo>
<sqx-notifo position="bottom-right"></sqx-notifo>
<sqx-notification-dropdown *ngIf="!isNotifoConfigured"></sqx-notification-dropdown>
</ul>

2
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;
}

Loading…
Cancel
Save