From 26ba91b86b7e047cd1e58e3b2a139d8f42f228d8 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 24 Oct 2023 14:08:25 +0200 Subject: [PATCH] Collaborations2 (#1036) * Describe graphql. * Initial tests. * Fixes. * A lot of progress. * Set other tests. * YDotnet * Simpler completion. * Use locking. * Remove comment tests. --- .../Actions/Comment/CommentActionHandler.cs | 32 +- .../Notification/NotificationActionHandler.cs | 30 +- .../OldEvents}/CommentDeleted.cs | 4 +- .../OldEvents}/CommentUpdated.cs | 4 +- .../Comments/Comment.cs | 2 +- ...quidex.Domain.Apps.Entities.MongoDb.csproj | 2 +- .../Backup/BackupProcessor.cs | 1 - .../Billing/UsageNotifierWorker.cs | 2 +- .../CommentCollaborationHandler.cs | 266 ++++++ .../CommentTriggerHandler.cs | 3 +- .../EmailUserNotificationOptions.cs | 2 +- .../EmailUserNotifications.cs | 2 +- .../Collaboration/ICollaborationService.cs | 23 + .../IUserNotifications.cs | 2 +- .../NoopUserNotifications.cs | 2 +- .../Comments/Commands/CommentTextCommand.cs | 15 - .../Comments/Commands/CreateComment.cs | 22 - .../Comments/Commands/DeleteComment.cs | 12 - .../Comments/Commands/UpdateComment.cs | 12 - .../Comments/Commands/_CommentsCommand.cs | 48 -- .../Comments/CommentsLoader.cs | 32 - .../Comments/CommentsResult.cs | 95 --- .../DomainObject/CommentsCommandMiddleware.cs | 76 -- .../Comments/DomainObject/CommentsStream.cs | 167 ---- .../DomainObject/Guards/GuardComments.cs | 87 -- .../Comments/ICommentsLoader.cs | 16 - .../Comments/IWatchingService.cs | 16 - .../Comments/WatchingService.cs | 61 -- .../Invitation/InvitationEventConsumer.cs | 2 +- .../Squidex.Domain.Apps.Entities.csproj | 5 + .../Comments/CommentCreated.cs | 7 +- .../Comments/CommentsEvent.cs | 17 - .../Squidex.Domain.Users.MongoDb.csproj | 2 +- .../EventSourcing/GetEventStore.cs | 23 - .../EventSourcing/Utils.cs | 1 - .../EventSourcing/MongoEventStore_Reader.cs | 22 - .../Squidex.Infrastructure.MongoDb.csproj | 4 +- .../src/Squidex.Infrastructure/DomainId.cs | 6 + .../EventSourcing/IEventStore.cs | 3 - .../Controllers/Apps/AppAssetsController.cs | 10 +- .../Controllers/Apps/AppClientsController.cs | 20 +- .../Apps/AppContributorsController.cs | 18 +- .../Controllers/Apps/AppImageController.cs | 4 +- .../Apps/AppLanguagesController.cs | 22 +- .../Controllers/Apps/AppRolesController.cs | 26 +- .../Controllers/Apps/AppSettingsController.cs | 10 +- .../Apps/AppWorkflowsController.cs | 20 +- .../Api/Controllers/Apps/AppsController.cs | 40 +- .../Assets/AssetContentController.cs | 8 +- .../Assets/AssetFoldersController.cs | 26 +- .../Controllers/Assets/AssetsController.cs | 74 +- .../Backups/BackupContentController.cs | 8 +- .../Controllers/Backups/BackupsController.cs | 14 +- .../Controllers/Backups/RestoreController.cs | 4 +- .../Comments/CommentsController.cs | 150 +--- .../Controllers/Comments/Models/CommentDto.cs | 56 -- .../Comments/Models/CommentsDto.cs | 47 -- .../Comments/Models/UpsertCommentDto.cs | 46 -- .../UserNotificationsController.cs | 99 --- .../Contents/ContentsController.cs | 96 +-- .../Contents/ContentsSharedController.cs | 30 +- .../Diagnostics/DiagnosticsController.cs | 8 +- .../EventConsumersController.cs | 14 +- .../Controllers/History/HistoryController.cs | 8 +- .../Languages/LanguagesController.cs | 2 +- .../Api/Controllers/News/NewsController.cs | 2 +- .../Api/Controllers/Ping/PingController.cs | 6 +- .../Controllers/Plans/AppPlansController.cs | 10 +- .../Controllers/Plans/TeamPlansController.cs | 8 +- .../Api/Controllers/Rules/RulesController.cs | 68 +- .../Schemas/SchemaFieldsController.cs | 118 +-- .../Controllers/Schemas/SchemasController.cs | 62 +- .../Controllers/Search/SearchController.cs | 4 +- .../Statistics/UsagesController.cs | 28 +- .../Teams/TeamContributorsController.cs | 18 +- .../Api/Controllers/Teams/TeamsController.cs | 18 +- .../Templates/TemplatesController.cs | 6 +- .../Translations/TranslationsController.cs | 4 +- .../Areas/Api/Controllers/UI/MyUIOptions.cs | 3 + .../Areas/Api/Controllers/UI/UIController.cs | 34 +- .../Users/UserManagementController.cs | 34 +- .../Api/Controllers/Users/UsersController.cs | 14 +- ...nsServices.cs => CollaborationServices.cs} | 10 +- .../Squidex/Config/Domain/CommandsServices.cs | 4 - .../Squidex/Config/Domain/CommentsServices.cs | 22 - .../Squidex/Config/Domain/FontendServices.cs | 16 + .../src/Squidex/Config/Domain/RuleServices.cs | 2 +- .../Squidex/Config/Domain/StoreServices.cs | 7 + backend/src/Squidex/Config/Web/WebServices.cs | 3 + backend/src/Squidex/Squidex.csproj | 8 +- backend/src/Squidex/Startup.cs | 3 +- .../Billing/UsageNotifierWorkerTest.cs | 2 +- .../CommentCollaborationHandlerTests.cs | 335 ++++++++ .../CommentTriggerHandlerTests.cs | 8 +- .../EmailUserNotificationsTests.cs | 2 +- .../Comments/CommentsLoaderTests.cs | 46 -- .../CommentsCommandMiddlewareTests.cs | 175 ---- .../DomainObject/CommentsStreamTests.cs | 175 ---- .../DomainObject/Guards/GuardCommentsTests.cs | 191 ----- .../Comments/WatchingServiceTests.cs | 82 -- .../InvitationEventConsumerTests.cs | 3 +- .../Squidex.Domain.Apps.Entities.Tests.csproj | 1 + .../EventSourcing/EventStoreTests.cs | 21 - frontend/package-lock.json | 388 +++++++-- frontend/package.json | 2 + .../event-consumers-page.component.ts | 9 +- .../pages/users/user-page.component.ts | 9 +- .../pages/users/users-page.component.ts | 10 +- .../assets/pages/assets-page.component.ts | 6 +- .../content/content-history-page.component.ts | 9 +- .../pages/content/content-page.component.html | 2 +- .../pages/content/content-page.component.ts | 27 +- .../editor/content-editor.component.html | 24 +- .../editor/content-editor.component.scss | 4 + .../editor/content-field.component.html | 80 +- .../pages/contents/contents-page.component.ts | 15 +- .../references/references-page.component.ts | 13 +- .../shared/content-extension.component.ts | 6 +- .../shared/forms/assets-editor.component.ts | 6 +- .../shared/forms/component.component.ts | 11 +- .../shared/forms/iframe-editor.component.ts | 9 +- .../forms/stock-photo-editor.component.ts | 12 +- .../references/content-creator.component.ts | 5 +- .../reference-dropdown.component.ts | 5 +- .../references-checkboxes.component.ts | 5 +- .../references/references-tags.component.ts | 5 +- .../pages/dashboard-page.component.ts | 14 +- .../events/rule-events-page.component.ts | 9 +- .../rules/pages/rule/rule-page.component.ts | 8 +- .../rule-simulator-page.component.ts | 11 +- .../fields/types/number-ui.component.ts | 12 +- .../fields/types/string-ui.component.ts | 13 +- .../types/string-validation.component.ts | 10 +- .../pages/schema/schema-page.component.ts | 9 +- .../pages/schemas/schemas-page.component.ts | 11 +- .../asset-scripts-page.component.ts | 5 +- .../pages/backups/backups-page.component.ts | 9 +- .../pages/more/more-page.component.ts | 9 +- .../pages/settings/settings-page.component.ts | 9 +- .../templates/templates-page.component.ts | 5 +- .../workflows/workflows-page.component.ts | 9 +- .../dashboard/dashboard-page.component.ts | 14 +- .../teams/pages/more/more-page.component.ts | 9 +- .../angular/compensate-scrollbar.directive.ts | 9 +- .../angular/forms/control-errors.component.ts | 7 +- .../forms/editors/autocomplete.component.ts | 5 +- .../editors/date-time-editor.component.ts | 7 +- .../forms/editors/dropdown.component.ts | 5 +- .../forms/editors/tag-editor.component.ts | 5 +- .../angular/image-source.directive.ts | 14 +- .../framework/angular/image-url.directive.ts | 11 +- .../modals/dialog-renderer.component.ts | 14 +- .../angular/modals/modal.directive.ts | 12 +- .../framework/angular/pipes/array.pipes.ts | 18 + .../angular/pipes/colors.pipes.spec.ts | 22 +- .../framework/angular/pipes/colors.pipes.ts | 149 +--- .../angular/pipes/date-time.pipes.spec.ts | 141 ++-- .../angular/pipes/date-time.pipes.ts | 56 +- .../framework/angular/resized.directive.ts | 11 +- .../angular/routers/parent-link.directive.ts | 10 +- .../framework/angular/stateful.component.ts | 55 +- .../app/framework/angular/subscriptions.ts | 50 ++ .../framework/angular/sync-width.directive.ts | 10 +- frontend/src/app/framework/internal.ts | 1 + frontend/src/app/framework/module.ts | 4 +- .../app/framework/utils/color-helper.spec.ts | 38 + .../src/app/framework/utils/color-helper.ts | 162 ++++ frontend/src/app/framework/utils/picasso.ts | 18 +- .../app/framework/utils/string-helper.spec.ts | 12 + .../src/app/framework/utils/string-helper.ts | 17 + .../comments/comment.component.html | 2 +- .../components/comments/comment.component.ts | 18 +- .../comments/comments.component.html | 49 +- .../components/comments/comments.component.ts | 37 +- .../contents/content-list-cell.directive.ts | 22 +- .../contents/content-value.component.ts | 8 +- .../shared/components/cursors.component.html | 11 + .../shared/components/cursors.component.scss | 19 + .../shared/components/cursors.component.ts | 38 + .../shared/components/cursors.directive.ts | 38 + .../components/focus-marker.component.html | 11 + .../components/focus-marker.component.scss | 23 + .../components/focus-marker.component.ts | 61 ++ .../references/content-selector.component.ts | 8 +- .../shared/components/tour-guide.component.ts | 8 +- .../shared/components/tour-hint.directive.ts | 11 +- .../components/watching-users.component.html | 8 +- .../components/watching-users.component.scss | 8 +- .../components/watching-users.component.ts | 23 +- frontend/src/app/shared/declarations.ts | 7 +- frontend/src/app/shared/internal.ts | 4 +- frontend/src/app/shared/module.ts | 9 +- .../services/collaboration.service.spec.ts | 177 ++++ .../shared/services/collaboration.service.ts | 184 +++++ .../shared/services/comments.service.spec.ts | 144 ---- .../app/shared/services/comments.service.ts | 121 --- .../src/app/shared/state/comments.form.ts | 4 +- .../app/shared/state/comments.state.spec.ts | 116 --- .../src/app/shared/state/comments.state.ts | 124 --- frontend/src/app/shared/state/comments.ts | 23 + .../src/app/shared/state/contents.forms.ts | 7 +- .../pages/internal/internal-area.component.ts | 9 +- .../notification-dropdown.component.html | 11 +- .../notification-dropdown.component.ts | 98 ++- .../notifications-menu.component.html | 2 +- .../pages/internal/profile-menu.component.ts | 6 +- .../src/app/theme/icomoon/demo-files/demo.css | 2 +- frontend/src/app/theme/icomoon/demo.html | 774 ++++++++++++------ .../src/app/theme/icomoon/fonts/icomoon.eot | Bin 36344 -> 36476 bytes .../src/app/theme/icomoon/fonts/icomoon.svg | 1 + .../src/app/theme/icomoon/fonts/icomoon.ttf | Bin 36180 -> 36312 bytes .../src/app/theme/icomoon/fonts/icomoon.woff | Bin 36256 -> 36388 bytes frontend/src/app/theme/icomoon/selection.json | 2 +- frontend/src/app/theme/icomoon/style.css | 242 +++--- frontend/src/config/webpack.config.js | 6 + .../TestSuite.ApiTests/CommentsTests.cs | 111 --- 216 files changed, 3746 insertions(+), 4073 deletions(-) rename backend/src/{Squidex.Domain.Apps.Events/Comments => Migrations/OldEvents}/CommentDeleted.cs (82%) rename backend/src/{Squidex.Domain.Apps.Events/Comments => Migrations/OldEvents}/CommentUpdated.cs (83%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs rename backend/src/Squidex.Domain.Apps.Entities/{Comments => Collaboration}/CommentTriggerHandler.cs (98%) rename backend/src/Squidex.Domain.Apps.Entities/{Notifications => Collaboration}/EmailUserNotificationOptions.cs (94%) rename backend/src/Squidex.Domain.Apps.Entities/{Notifications => Collaboration}/EmailUserNotifications.cs (99%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Collaboration/ICollaborationService.cs rename backend/src/Squidex.Domain.Apps.Entities/{Notifications => Collaboration}/IUserNotifications.cs (94%) rename backend/src/Squidex.Domain.Apps.Entities/{Notifications => Collaboration}/NoopUserNotifications.cs (95%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs delete mode 100644 backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs delete mode 100644 backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs delete mode 100644 backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs delete mode 100644 backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs rename backend/src/Squidex/Config/Domain/{NotificationsServices.cs => CollaborationServices.cs} (79%) delete mode 100644 backend/src/Squidex/Config/Domain/CommentsServices.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentCollaborationHandlerTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/{Comments => Collaboration}/CommentTriggerHandlerTests.cs (98%) rename backend/tests/Squidex.Domain.Apps.Entities.Tests/{Notifications => Collaboration}/EmailUserNotificationsTests.cs (99%) delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/Guards/GuardCommentsTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs create mode 100644 frontend/src/app/framework/angular/pipes/array.pipes.ts create mode 100644 frontend/src/app/framework/angular/subscriptions.ts create mode 100644 frontend/src/app/framework/utils/color-helper.spec.ts create mode 100644 frontend/src/app/framework/utils/color-helper.ts create mode 100644 frontend/src/app/shared/components/cursors.component.html create mode 100644 frontend/src/app/shared/components/cursors.component.scss create mode 100644 frontend/src/app/shared/components/cursors.component.ts create mode 100644 frontend/src/app/shared/components/cursors.directive.ts create mode 100644 frontend/src/app/shared/components/focus-marker.component.html create mode 100644 frontend/src/app/shared/components/focus-marker.component.scss create mode 100644 frontend/src/app/shared/components/focus-marker.component.ts create mode 100644 frontend/src/app/shared/services/collaboration.service.spec.ts create mode 100644 frontend/src/app/shared/services/collaboration.service.ts delete mode 100644 frontend/src/app/shared/services/comments.service.spec.ts delete mode 100644 frontend/src/app/shared/services/comments.service.ts delete mode 100644 frontend/src/app/shared/state/comments.state.spec.ts delete mode 100644 frontend/src/app/shared/state/comments.state.ts create mode 100644 frontend/src/app/shared/state/comments.ts delete mode 100644 tools/TestSuite/TestSuite.ApiTests/CommentsTests.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs index e6b28a8b3..8e785b134 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs @@ -7,31 +7,31 @@ using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; -using Squidex.Domain.Apps.Entities.Comments.Commands; +using Squidex.Domain.Apps.Entities.Collaboration; +using Squidex.Domain.Apps.Events.Comments; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; namespace Squidex.Extensions.Actions.Comment; -public sealed class CommentActionHandler : RuleActionHandler +public sealed class CommentActionHandler : RuleActionHandler { private const string Description = "Send a Comment"; - private readonly ICommandBus commandBus; + private readonly ICollaborationService collaboration; - public CommentActionHandler(RuleEventFormatter formatter, ICommandBus commandBus) + public CommentActionHandler(RuleEventFormatter formatter, ICollaborationService collaboration) : base(formatter) { - this.commandBus = commandBus; + this.collaboration = collaboration; } - protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action) + protected override async Task<(string Description, CommentCreated Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action) { if (@event is not EnrichedContentEvent contentEvent) { - return ("Ignore", new CreateComment()); + return ("Ignore", new CommentCreated()); } - var ruleJob = new CreateComment + var ruleJob = new CommentCreated { AppId = contentEvent.AppId }; @@ -40,7 +40,7 @@ public sealed class CommentActionHandler : RuleActionHandler ExecuteJobAsync(CreateComment job, + protected override async Task ExecuteJobAsync(CommentCreated job, CancellationToken ct = default) { - var command = job; - - if (command.CommentsId == default) + if (job.CommentsId == default) { return Result.Ignored(); } - command.FromRule = true; - - await commandBus.PublishAsync(command, ct); + await collaboration.CommentAsync(job.AppId, job.CommentsId, job.Text, job.Actor, job.Url, true, ct); - return Result.Success($"Commented: {command.Text}"); + return Result.Success($"Commented: {job.Text}"); } } diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs index 261de6e96..732b3ae60 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs @@ -7,32 +7,31 @@ using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; -using Squidex.Domain.Apps.Entities.Comments.Commands; +using Squidex.Domain.Apps.Entities.Collaboration; +using Squidex.Domain.Apps.Events.Comments; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Shared.Users; namespace Squidex.Extensions.Actions.Notification; -public sealed class NotificationActionHandler : RuleActionHandler +public sealed class NotificationActionHandler : RuleActionHandler { private const string Description = "Send a Notification"; - private readonly ICommandBus commandBus; + private readonly ICollaborationService collaboration; private readonly IUserResolver userResolver; - public NotificationActionHandler(RuleEventFormatter formatter, ICommandBus commandBus, IUserResolver userResolver) + public NotificationActionHandler(RuleEventFormatter formatter, ICollaborationService collaboration, IUserResolver userResolver) : base(formatter) { - this.commandBus = commandBus; - + this.collaboration = collaboration; this.userResolver = userResolver; } - protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action) + protected override async Task<(string Description, CommentCreated Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action) { if (@event is not EnrichedUserEventBase userEvent) { - return ("Ignore", new CreateComment()); + return ("Ignore", new CommentCreated()); } var user = await userResolver.FindByIdOrEmailAsync(action.User); @@ -49,9 +48,8 @@ public sealed class NotificationActionHandler : RuleActionHandler ExecuteJobAsync(CreateComment job, + protected override async Task ExecuteJobAsync(CommentCreated job, CancellationToken ct = default) { - var command = job; - - if (command.CommentsId == default) + if (job.CommentsId == default) { return Result.Ignored(); } - await commandBus.PublishAsync(command, ct); + await collaboration.NotifyAsync(job.CommentsId.ToString(), job.Text, job.Actor, job.Url, true, ct); - return Result.Success($"Notified: {command.Text}"); + return Result.Success($"Notified: {job.Text}"); } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs b/backend/src/Migrations/OldEvents/CommentDeleted.cs similarity index 82% rename from backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs rename to backend/src/Migrations/OldEvents/CommentDeleted.cs index f6282e2e0..efd794251 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs +++ b/backend/src/Migrations/OldEvents/CommentDeleted.cs @@ -7,9 +7,9 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Comments; +namespace Migrations.OldEvents; [EventType(nameof(CommentDeleted))] -public sealed class CommentDeleted : CommentsEvent +public sealed class CommentDeleted : IEvent { } diff --git a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs b/backend/src/Migrations/OldEvents/CommentUpdated.cs similarity index 83% rename from backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs rename to backend/src/Migrations/OldEvents/CommentUpdated.cs index dc314d772..7a490f196 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs +++ b/backend/src/Migrations/OldEvents/CommentUpdated.cs @@ -7,10 +7,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Events.Comments; +namespace Migrations.OldEvents; [EventType(nameof(CommentUpdated))] -public sealed class CommentUpdated : CommentsEvent +public sealed class CommentUpdated : IEvent { public string Text { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs index 8c3a5224d..eb47a2d3d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Comments; -public sealed record Comment(DomainId Id, Instant Time, RefToken User, string Text, Uri? Url = null) +public sealed record Comment(Instant Time, RefToken User, string Text, Uri? Url = null, bool SkipHandlers = false) { public RefToken User { get; } = Guard.NotNull(User); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index c030c99e5..f247b82a5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -23,7 +23,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs index ef1c359fa..b61844a62 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using NodaTime; using Squidex.Domain.Apps.Entities.Backup.State; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs index 36b14bcb5..b16fdbaa3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Billing/UsageNotifierWorker.cs @@ -6,7 +6,7 @@ // ========================================================================== using NodaTime; -using Squidex.Domain.Apps.Entities.Notifications; +using Squidex.Domain.Apps.Entities.Collaboration; using Squidex.Hosting; using Squidex.Infrastructure; using Squidex.Infrastructure.States; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs new file mode 100644 index 000000000..dc8659ff0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs @@ -0,0 +1,266 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using NodaTime; +using Squidex.Domain.Apps.Core.Comments; +using Squidex.Domain.Apps.Events.Comments; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared.Users; +using YDotNet.Document.Cells; +using YDotNet.Document.Types.Events; +using YDotNet.Extensions; +using YDotNet.Server; + +namespace Squidex.Domain.Apps.Entities.Collaboration; + +public sealed partial class CommentCollaborationHandler : IDocumentCallback, ICollaborationService +{ + private static readonly Regex MentionRegex = BuildMentionRegex(); + private readonly IJsonSerializer jsonSerializer; + private readonly IEventStore eventStore; + private readonly IEventFormatter eventFormatter; + private readonly IUserResolver userResolver; + private readonly IClock clock; + private readonly ILogger log; + private IDocumentManager? currentManager; + + public Task LastTask { get; private set; } + + public CommentCollaborationHandler( + IJsonSerializer jsonSerializer, + IEventStore eventStore, + IEventFormatter eventFormatter, + IUserResolver userResolver, + IClock clock, + ILogger log) + { + this.jsonSerializer = jsonSerializer; + this.eventStore = eventStore; + this.eventFormatter = eventFormatter; + this.userResolver = userResolver; + this.clock = clock; + this.log = log; + } + + public ValueTask OnInitializedAsync(IDocumentManager manager) + { + currentManager = manager; + return default; + } + + public Task NotifyAsync(string userId, string text, RefToken actor, Uri? url, bool skipHandlers, + CancellationToken ct = default) + { + return CommentAsync(UserDocument(userId), text, actor, url, skipHandlers, ct); + } + + public Task CommentAsync(NamedId appId, DomainId resourceId, string text, RefToken actor, Uri? url, bool skipHandlers, + CancellationToken ct = default) + { + return CommentAsync(ResourceDocument(appId, resourceId), text, actor, url, skipHandlers, ct); + } + + private async Task CommentAsync(string documentName, string text, RefToken actor, Uri? url, bool skipHandlers, + CancellationToken ct) + { + if (currentManager == null) + { + return; + } + + var notificationsContext = new DocumentContext(documentName, 0); + + // Use the update method to ensure that only one thread has access to the doc. + await currentManager.UpdateDocAsync(notificationsContext, doc => + { + var stream = doc.Array("stream"); + + using (var transaction = doc.WriteTransaction()) + { + var commentValue = new Comment(clock.GetCurrentInstant(), actor, text, url, skipHandlers); + var commentJson = jsonSerializer.Serialize(commentValue); + + stream.InsertRange(transaction, stream.Length, InputFactory.FromJson(commentJson)); + } + }, ct); + } + + public ValueTask OnDocumentLoadedAsync(DocumentLoadEvent @event) + { + if (!IsResourceDocument(@event.Context.DocumentName, out var appId, out var resourceId)) + { + return default; + } + + var stream = @event.Document.Array("stream"); + + stream.ObserveDeep(changes => + { + var newComments = + changes + .Where(x => x.Tag == EventBranchTag.Array) + .Select(x => x.ArrayEvent) + .SelectMany(x => x.Delta).Where(x => x.Tag == EventChangeTag.Add) + .SelectMany(x => x.Values).Where(x => x.Tag == OutputTag.JsonObject) + .ToArray(); + + if (newComments.Length == 0) + { + // Just store the last task for tests. + LastTask = Task.CompletedTask; + return; + } + + LastTask = Task.Run(async () => + { + try + { + // Run in an extra task to prevent deadlocks with the outer transaction. + await HandleAsync(@event, appId, resourceId, newComments); + } + catch (Exception ex) + { + // We are in an extra task, so the exception would be probably swallowed. + log.LogError(ex, "Failed to handle yjs event."); + throw; + } + }); + }); + + return default; + } + + private async Task HandleAsync(DocumentLoadEvent @event, NamedId appId, DomainId resourceId, Output[] newComments) + { + var comments = new List(); + + // Use the update method to ensure that only one thread has access to the doc. + await @event.Source.UpdateDocAsync(@event.Context, (doc) => + { + using (var transaction = @event.Document.ReadTransaction()) + { + foreach (var output in newComments) + { + // Just use the json string for debuggability. + var json = output.ToJson(transaction); + + var comment = jsonSerializer.Deserialize(json); + + if (!comment.SkipHandlers) + { + comments.Add(comment); + } + } + } + }); + + var streamName = $"comments-{DomainId.Combine(appId, resourceId)}"; + + foreach (var comment in comments) + { + var commentEvent = await CreateEventAsync(comment, appId, resourceId); + + var eventBody = Envelope.Create(commentEvent); + var eventData = eventFormatter.ToEventData(eventBody, Guid.NewGuid()); + + await eventStore.AppendAsync(Guid.NewGuid(), streamName, EtagVersion.Any, new List { eventData }); + + foreach (var mentionedUser in commentEvent.Mentions.OrEmpty()) + { + await NotifyAsync(mentionedUser, comment.Text, RefToken.User(mentionedUser), comment.Url, true); + } + } + } + + private async Task CreateEventAsync(Comment comment, NamedId appId, DomainId commentsId) + { + var @event = new CommentCreated + { + Actor = comment.User, + CommentId = DomainId.NewGuid(), + CommentsId = commentsId, + AppId = appId, + }; + + SimpleMapper.Map(comment, @event); + + await MentionUsersAsync(@event); + return @event; + } + + private async Task MentionUsersAsync(CommentCreated comment) + { + if (string.IsNullOrWhiteSpace(comment.Text)) + { + return; + } + + var emails = MentionRegex.Matches(comment.Text).Select(x => x.Value[1..]).ToArray(); + + if (emails.Length == 0) + { + return; + } + + var mentions = new List(); + + foreach (var email in emails) + { + var user = await userResolver.FindByIdOrEmailAsync(email); + + if (user != null) + { + mentions.Add(user.Id); + } + } + + if (mentions.Count > 0) + { + comment.Mentions = mentions.ToArray(); + } + } + + public string UserDocument(string userId) + { + return $"users/{userId}"; + } + + public string ResourceDocument(NamedId appId, DomainId resourceId) + { + return $"apps/{appId}/{resourceId}"; + } + + private static bool IsResourceDocument(string name, out NamedId appId, out DomainId resourceId) + { + resourceId = default; + + if (!name.StartsWith("apps", StringComparison.Ordinal)) + { + appId = default!; + return false; + } + + var parts = name.Split('/'); + + if (parts.Length < 3 || !NamedId.TryParse(parts[1], DomainId.TryParse, out appId!)) + { + appId = default!; + return false; + } + + resourceId = DomainId.Create(string.Join('/', parts.Skip(2))); + return true; + } + + [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)] + private static partial Regex BuildMentionRegex(); +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentTriggerHandler.cs similarity index 98% rename from backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs rename to backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentTriggerHandler.cs index 48638d957..bc893b434 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentTriggerHandler.cs @@ -17,7 +17,7 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Comments; +namespace Squidex.Domain.Apps.Entities.Collaboration; public sealed class CommentTriggerHandler : IRuleTriggerHandler { @@ -29,7 +29,6 @@ public sealed class CommentTriggerHandler : IRuleTriggerHandler public CommentTriggerHandler(IScriptEngine scriptEngine, IUserResolver userResolver) { this.scriptEngine = scriptEngine; - this.userResolver = userResolver; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotificationOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/EmailUserNotificationOptions.cs similarity index 94% rename from backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotificationOptions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Collaboration/EmailUserNotificationOptions.cs index 084779b28..1936a2b9f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotificationOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/EmailUserNotificationOptions.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Domain.Apps.Entities.Notifications; +namespace Squidex.Domain.Apps.Entities.Collaboration; public sealed class EmailUserNotificationOptions { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/EmailUserNotifications.cs similarity index 99% rename from backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs rename to backend/src/Squidex.Domain.Apps.Entities/Collaboration/EmailUserNotifications.cs index 932625dd0..bf7721e00 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/EmailUserNotifications.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure.Email; using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Notifications; +namespace Squidex.Domain.Apps.Entities.Collaboration; public sealed class EmailUserNotifications : IUserNotifications { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Collaboration/ICollaborationService.cs b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/ICollaborationService.cs new file mode 100644 index 000000000..8b8bf67c2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/ICollaborationService.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Collaboration; + +public interface ICollaborationService +{ + Task NotifyAsync(string userId, string text, RefToken actor, Uri? url, bool skipHandlers, + CancellationToken ct = default); + + Task CommentAsync(NamedId appId, DomainId resourceId, string text, RefToken actor, Uri? url, bool skipHandlers, + CancellationToken ct = default); + + string UserDocument(string userId); + + string ResourceDocument(NamedId appId, DomainId resourceId); +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/IUserNotifications.cs b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/IUserNotifications.cs similarity index 94% rename from backend/src/Squidex.Domain.Apps.Entities/Notifications/IUserNotifications.cs rename to backend/src/Squidex.Domain.Apps.Entities/Collaboration/IUserNotifications.cs index c66ddf8a4..a7fbf5b35 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/IUserNotifications.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/IUserNotifications.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Teams; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Notifications; +namespace Squidex.Domain.Apps.Entities.Collaboration; public interface IUserNotifications { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopUserNotifications.cs b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/NoopUserNotifications.cs similarity index 95% rename from backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopUserNotifications.cs rename to backend/src/Squidex.Domain.Apps.Entities/Collaboration/NoopUserNotifications.cs index 19d61f210..ca80e1dc9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NoopUserNotifications.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Collaboration/NoopUserNotifications.cs @@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Teams; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Notifications; +namespace Squidex.Domain.Apps.Entities.Collaboration; public sealed class NoopUserNotifications : IUserNotifications { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs deleted file mode 100644 index 7367eb270..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Comments.Commands; - -public abstract class CommentTextCommand : CommentCommand -{ - public string Text { get; set; } - - public string[]? Mentions { get; set; } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs deleted file mode 100644 index 6934c166b..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Comments.Commands; - -public sealed class CreateComment : CommentTextCommand -{ - public bool IsMention { get; set; } - - public Uri? Url { get; set; } - - public CreateComment() - { - CommentId = DomainId.NewGuid(); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs deleted file mode 100644 index 8281697a7..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs +++ /dev/null @@ -1,12 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Comments.Commands; - -public sealed class DeleteComment : CommentCommand -{ -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs deleted file mode 100644 index 46307f83e..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs +++ /dev/null @@ -1,12 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Entities.Comments.Commands; - -public sealed class UpdateComment : CommentTextCommand -{ -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs deleted file mode 100644 index 8a811b85e..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/_CommentsCommand.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -#pragma warning disable MA0048 // File name must match type name - -namespace Squidex.Domain.Apps.Entities.Comments.Commands; - -public abstract class CommentCommand : CommentsCommand -{ - public DomainId CommentId { get; set; } -} - -public abstract class CommentsCommand : CommentsCommandBase -{ - public static readonly NamedId NoApp = NamedId.Of(DomainId.Empty, "none"); - - public DomainId CommentsId { get; set; } - - public override DomainId AggregateId - { - get - { - if (AppId.Id == default) - { - return CommentsId; - } - else - { - return DomainId.Combine(AppId, CommentsId); - } - } - } -} - -// This command is needed as marker for middlewares. -public abstract class CommentsCommandBase : SquidexCommand, IAppCommand, IAggregateCommand -{ - public NamedId AppId { get; set; } - - public abstract DomainId AggregateId { get; } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs deleted file mode 100644 index c3c02bdd8..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Comments.DomainObject; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Comments; - -public sealed class CommentsLoader : ICommentsLoader -{ - private readonly IDomainObjectFactory domainObjectFactory; - - public CommentsLoader(IDomainObjectFactory domainObjectFactory) - { - this.domainObjectFactory = domainObjectFactory; - } - - public async Task GetCommentsAsync(DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default) - { - var stream = domainObjectFactory.Create(id); - - await stream.LoadAsync(ct); - - return stream.GetComments(version); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs deleted file mode 100644 index dd591e04d..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs +++ /dev/null @@ -1,95 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Comments; -using Squidex.Domain.Apps.Events.Comments; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.Comments; - -public sealed class CommentsResult -{ - public List CreatedComments { get; set; } = new List(); - - public List UpdatedComments { get; set; } = new List(); - - public List DeletedComments { get; set; } = new List(); - - public long Version { get; set; } - - public static CommentsResult FromEvents(IEnumerable> events, long currentVersion, int lastVersion) - { - var result = new CommentsResult { Version = currentVersion }; - - foreach (var @event in events.Skip(lastVersion < 0 ? 0 : lastVersion + 1)) - { - switch (@event.Payload) - { - case CommentDeleted deleted: - { - var id = deleted.CommentId; - - if (result.CreatedComments.Exists(x => x.Id == id)) - { - result.CreatedComments.RemoveAll(x => x.Id == id); - } - else if (result.UpdatedComments.Exists(x => x.Id == id)) - { - result.UpdatedComments.RemoveAll(x => x.Id == id); - result.DeletedComments.Add(id); - } - else - { - result.DeletedComments.Add(id); - } - - break; - } - - case CommentCreated created: - { - var comment = new Comment( - created.CommentId, - @event.Headers.Timestamp(), - @event.Payload.Actor, - created.Text, - created.Url); - - result.CreatedComments.Add(comment); - break; - } - - case CommentUpdated updated: - { - var id = updated.CommentId; - - var comment = new Comment( - id, - @event.Headers.Timestamp(), - @event.Payload.Actor, - updated.Text, - null); - - if (result.CreatedComments.Exists(x => x.Id == id)) - { - result.CreatedComments.RemoveAll(x => x.Id == id); - result.CreatedComments.Add(comment); - } - else - { - result.UpdatedComments.Add(comment); - } - - break; - } - } - } - - return result; - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs deleted file mode 100644 index 3b2239dab..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs +++ /dev/null @@ -1,76 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Text.RegularExpressions; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Infrastructure.Commands; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject; - -public sealed class CommentsCommandMiddleware : AggregateCommandMiddleware -{ - private static readonly Regex MentionRegex = new Regex(@"@(?=.{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, TimeSpan.FromMilliseconds(100)); - private readonly IUserResolver userResolver; - - public CommentsCommandMiddleware(IDomainObjectFactory domainObjectFactory, IUserResolver userResolver) - : base(domainObjectFactory) - { - this.userResolver = userResolver; - } - - public override async Task HandleAsync(CommandContext context, NextDelegate next, - CancellationToken ct) - { - if (context.Command is CommentsCommand commentsCommand) - { - if (commentsCommand is CreateComment createComment && !IsMention(createComment)) - { - await MentionUsersAsync(createComment); - } - } - - await base.HandleAsync(context, next, ct); - } - - private static bool IsMention(CreateComment createComment) - { - return createComment.IsMention; - } - - private async Task MentionUsersAsync(CommentTextCommand command) - { - if (string.IsNullOrWhiteSpace(command.Text)) - { - return; - } - - var emails = MentionRegex.Matches(command.Text).Select(x => x.Value[1..]).ToArray(); - - if (emails.Length == 0) - { - return; - } - - var mentions = new List(); - - foreach (var email in emails) - { - var user = await userResolver.FindByIdOrEmailAsync(email); - - if (user != null) - { - mentions.Add(user.Id); - } - } - - if (mentions.Count > 0) - { - command.Mentions = mentions.ToArray(); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs deleted file mode 100644 index 1b087e470..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs +++ /dev/null @@ -1,167 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Entities.Comments.DomainObject.Guards; -using Squidex.Domain.Apps.Events.Comments; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject; - -public class CommentsStream : IAggregate -{ - private readonly List> uncommittedEvents = new List>(); - private readonly List> events = new List>(); - private readonly DomainId key; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly string streamName; - private long version = EtagVersion.Empty; - - private long Version => version; - - public CommentsStream( - DomainId key, - IEventFormatter eventFormatter, - IEventStore eventStore) - { - this.key = key; - this.eventFormatter = eventFormatter; - this.eventStore = eventStore; - - streamName = $"comments-{key}"; - } - - public virtual async Task LoadAsync( - CancellationToken ct) - { - var storedEvents = await eventStore.QueryStreamReverseAsync(streamName, 100, ct); - - foreach (var @event in storedEvents) - { - var parsedEvent = eventFormatter.Parse(@event); - - version = @event.EventStreamNumber; - - events.Add(parsedEvent.To()); - } - } - - public virtual async Task ExecuteAsync(IAggregateCommand command, - CancellationToken ct) - { - await LoadAsync(ct); - - switch (command) - { - case CreateComment createComment: - return await Upsert(createComment, c => - { - GuardComments.CanCreate(c); - - Create(c); - }, ct); - - case UpdateComment updateComment: - return await Upsert(updateComment, c => - { - GuardComments.CanUpdate(c, key.ToString(), events); - - Update(c); - }, ct); - - case DeleteComment deleteComment: - return await Upsert(deleteComment, c => - { - GuardComments.CanDelete(c, key.ToString(), events); - - Delete(c); - }, ct); - - default: - ThrowHelper.NotSupportedException(); - return null!; - } - } - - private async Task Upsert(TCommand command, Action handler, - CancellationToken ct) where TCommand : CommentsCommand - { - Guard.NotNull(command); - Guard.NotNull(handler); - - if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) - { - throw new DomainObjectVersionException(key.ToString(), Version, command.ExpectedVersion); - } - - var previousVersion = version; - - try - { - handler(command); - - if (uncommittedEvents.Count > 0) - { - var commitId = Guid.NewGuid(); - - var eventData = uncommittedEvents.Select(x => eventFormatter.ToEventData(x, commitId)).ToList(); - - await eventStore.AppendAsync(commitId, streamName, previousVersion, eventData, ct); - } - - events.AddRange(uncommittedEvents); - - return CommandResult.Empty(key, Version, previousVersion); - } - catch - { - version = previousVersion; - - throw; - } - finally - { - uncommittedEvents.Clear(); - } - } - - public void Create(CreateComment command) - { - RaiseEvent(SimpleMapper.Map(command, new CommentCreated())); - } - - public void Update(UpdateComment command) - { - RaiseEvent(SimpleMapper.Map(command, new CommentUpdated())); - } - - public void Delete(DeleteComment command) - { - RaiseEvent(SimpleMapper.Map(command, new CommentDeleted())); - } - - private void RaiseEvent(CommentsEvent @event) - { - uncommittedEvents.Add(Envelope.Create(@event)); - - version++; - } - - public virtual List> GetUncommittedEvents() - { - return uncommittedEvents; - } - - public virtual CommentsResult GetComments(long sinceVersion = EtagVersion.Any) - { - return CommentsResult.FromEvents(events, Version, (int)sinceVersion); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs deleted file mode 100644 index 68f7c406a..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Events.Comments; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Translations; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject.Guards; - -public static class GuardComments -{ - public static void CanCreate(CreateComment command) - { - Guard.NotNull(command); - - Validate.It(e => - { - if (string.IsNullOrWhiteSpace(command.Text)) - { - e(Not.Defined(nameof(command.Text)), nameof(command.Text)); - } - }); - } - - public static void CanUpdate(UpdateComment command, string commentsId, List> events) - { - Guard.NotNull(command); - - var comment = FindComment(events, command.CommentId); - - if (!string.Equals(commentsId, command.Actor.Identifier, StringComparison.Ordinal) && !comment.Payload.Actor.Equals(command.Actor)) - { - throw new DomainException(T.Get("comments.notUserComment")); - } - - Validate.It(e => - { - if (string.IsNullOrWhiteSpace(command.Text)) - { - e(Not.Defined(nameof(command.Text)), nameof(command.Text)); - } - }); - } - - public static void CanDelete(DeleteComment command, string commentsId, List> events) - { - Guard.NotNull(command); - - var comment = FindComment(events, command.CommentId); - - if (!string.Equals(commentsId, command.Actor.Identifier, StringComparison.Ordinal) && !comment.Payload.Actor.Equals(command.Actor)) - { - throw new DomainException(T.Get("comments.notUserComment")); - } - } - - private static Envelope FindComment(List> events, DomainId commentId) - { - Envelope? result = null; - - foreach (var @event in events) - { - if (@event.Payload is CommentCreated created && created.CommentId == commentId) - { - result = @event.To(); - } - else if (@event.Payload is CommentDeleted deleted && deleted.CommentId == commentId) - { - result = null; - } - } - - if (result == null) - { - throw new DomainObjectNotFoundException(commentId.ToString()); - } - - return result; - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs deleted file mode 100644 index 69a30c652..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Comments; - -public interface ICommentsLoader -{ - Task GetCommentsAsync(DomainId id, long version = EtagVersion.Any, - CancellationToken ct = default); -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs deleted file mode 100644 index 85df75ec7..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Comments; - -public interface IWatchingService -{ - Task GetWatchingUsersAsync(DomainId appId, string? resource, string userId, - CancellationToken ct = default); -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs deleted file mode 100644 index 4ed8372db..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Comments; - -public sealed class WatchingService : IWatchingService -{ - private readonly IPersistenceFactory persistenceFactory; - - [CollectionName("Watches")] - public sealed class State - { - private static readonly Duration Timeout = Duration.FromMinutes(1); - - public Dictionary Users { get; set; } = new Dictionary(); - - public (bool, string[]) Add(string watcherId, IClock clock) - { - var now = clock.GetCurrentInstant(); - - foreach (var (userId, lastSeen) in Users.ToList()) - { - var timeSinceLastSeen = now - lastSeen; - - if (timeSinceLastSeen > Timeout) - { - Users.Remove(userId); - } - } - - Users[watcherId] = now; - - return (true, Users.Keys.ToArray()); - } - } - - public IClock Clock { get; set; } = SystemClock.Instance; - - public WatchingService(IPersistenceFactory persistenceFactory) - { - this.persistenceFactory = persistenceFactory; - } - - public async Task GetWatchingUsersAsync(DomainId appId, string? resource, string userId, - CancellationToken ct = default) - { - var state = new SimpleState(persistenceFactory, GetType(), $"{appId}_{resource}"); - - await state.LoadAsync(ct); - - return await state.UpdateAsync(x => x.Add(userId, Clock), ct: ct); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs index 811cc6f07..6b381570c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Invitation/InvitationEventConsumer.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; using NodaTime; -using Squidex.Domain.Apps.Entities.Notifications; +using Squidex.Domain.Apps.Entities.Collaboration; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Teams; using Squidex.Infrastructure; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index d1e5d1318..abb308e63 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -30,6 +30,8 @@ + + @@ -52,4 +54,7 @@ Resources.Designer.cs + + + diff --git a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs index 79b202ded..c44590112 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs @@ -5,13 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Comments; [EventType(nameof(CommentCreated))] -public sealed class CommentCreated : CommentsEvent +public sealed class CommentCreated : AppEvent { + public DomainId CommentsId { get; set; } + + public DomainId CommentId { get; set; } + public string Text { get; set; } public string[]? Mentions { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs deleted file mode 100644 index 652358c85..000000000 --- a/backend/src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Events.Comments; - -public abstract class CommentsEvent : AppEvent -{ - public DomainId CommentsId { get; set; } - - public DomainId CommentId { get; set; } -} diff --git a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index 90c9fa556..6946662d9 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -24,7 +24,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs index 0ae89afa5..7519d9eab 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs @@ -89,29 +89,6 @@ public sealed class GetEventStore : IEventStore, IInitializable } } - public async Task> QueryStreamReverseAsync(string streamName, int count = int.MaxValue, - CancellationToken ct = default) - { - if (count <= 0) - { - return EmptyEvents; - } - - using (Telemetry.Activities.StartActivity("GetEventStore/GetEventStore")) - { - var result = new List(); - - var stream = QueryReverseAsync(streamName, StreamPosition.End, count, ct); - - await foreach (var storedEvent in stream.IgnoreNotFound(ct)) - { - result.Add(storedEvent); - } - - return result.ToList(); - } - } - public async Task> QueryStreamAsync(string streamName, long afterStreamPosition = EtagVersion.Empty, CancellationToken ct = default) { diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Utils.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Utils.cs index d9ee49ad6..9284cf20b 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Utils.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Utils.cs @@ -7,7 +7,6 @@ using System.Globalization; using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; using EventStore.Client; namespace Squidex.Infrastructure.EventSourcing; diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs index 7345bc320..d86fc0183 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -34,28 +34,6 @@ public partial class MongoEventStore : MongoRepositoryBase, IE } } - public async Task> QueryStreamReverseAsync(string streamName, int count = int.MaxValue, - CancellationToken ct = default) - { - if (count <= 0) - { - return EmptyEvents; - } - - using (Telemetry.Activities.StartActivity("MongoEventStore/QueryLatestAsync")) - { - var filter = FilterExtensions.ByStream(StreamFilter.Name(streamName)); - - var commits = - await Collection.Find(filter).Sort(Sort.Descending(x => x.Timestamp)).Limit(count) - .ToListAsync(ct); - - var result = commits.Select(x => x.Filtered()).Reverse().SelectMany(x => x).TakeLast(count).ToList(); - - return result; - } - } - public async Task> QueryStreamAsync(string streamName, long afterStreamPosition = EtagVersion.Empty, CancellationToken ct = default) { diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index 47516ece4..6da25bf78 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -18,8 +18,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/backend/src/Squidex.Infrastructure/DomainId.cs b/backend/src/Squidex.Infrastructure/DomainId.cs index 9cf9f3dfa..4c04eea93 100644 --- a/backend/src/Squidex.Infrastructure/DomainId.cs +++ b/backend/src/Squidex.Infrastructure/DomainId.cs @@ -102,4 +102,10 @@ public readonly struct DomainId : IEquatable, IComparable { return new DomainId($"{id1}{IdSeparator}{id2}"); } + + public static bool TryParse(ReadOnlySpan input, out DomainId result) + { + result = new DomainId(input.ToString()); + return true; + } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs index 314f109bc..8f63ee016 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs @@ -11,9 +11,6 @@ namespace Squidex.Infrastructure.EventSourcing; public interface IEventStore { - Task> QueryStreamReverseAsync(string streamName, int take = int.MaxValue, - CancellationToken ct = default); - Task> QueryStreamAsync(string streamName, long afterStreamPosition = EtagVersion.Empty, CancellationToken ct = default); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs index 71f924b53..f3e8cf647 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs @@ -29,8 +29,8 @@ public sealed class AppAssetsController : ApiController /// Get the app asset scripts. /// /// The name of the app to get the asset scripts for. - /// Asset scripts returned.. - /// App not found.. + /// Asset scripts returned. + /// App not found. [HttpGet] [Route("apps/{app}/assets/scripts")] [ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)] @@ -51,9 +51,9 @@ public sealed class AppAssetsController : ApiController /// /// The name of the app to update. /// The values to update. - /// Asset scripts updated.. - /// Asset request not valid.. - /// App not found.. + /// Asset scripts updated. + /// Asset request not valid. + /// App not found. [HttpPut] [Route("apps/{app}/assets/scripts")] [ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 42a06c553..6ccda9323 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -31,8 +31,8 @@ public sealed class AppClientsController : ApiController /// Get app clients. /// /// The name of the app. - /// Clients returned.. - /// App not found.. + /// Clients returned. + /// App not found. /// /// Gets all configured clients for the app with the specified name. /// @@ -58,9 +58,9 @@ public sealed class AppClientsController : ApiController /// /// The name of the app. /// Client object that needs to be added to the app. - /// Client created.. - /// Client request not valid.. - /// App not found.. + /// Client created. + /// Client request not valid. + /// App not found. /// /// Create a new client for the app with the specified name. /// The client secret is auto generated on the server and returned. The client does not expire, the access token is valid for 30 days. @@ -85,9 +85,9 @@ public sealed class AppClientsController : ApiController /// The name of the app. /// The ID of the client that must be updated. /// Client object that needs to be updated. - /// Client updated.. - /// Client request not valid.. - /// Client or app not found.. + /// Client updated. + /// Client request not valid. + /// Client or app not found. /// /// Only the display name can be changed, create a new client if necessary. /// @@ -110,8 +110,8 @@ public sealed class AppClientsController : ApiController /// /// The name of the app. /// The ID of the client that must be deleted. - /// Client deleted.. - /// Client or app not found.. + /// Client deleted. + /// Client or app not found. /// /// The application that uses this client credentials cannot access the API after it has been revoked. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index 17fd0c5ae..0a88d8d2d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -38,8 +38,8 @@ public sealed class AppContributorsController : ApiController /// Get app contributors. /// /// The name of the app. - /// Contributors returned.. - /// App not found.. + /// Contributors returned. + /// App not found. [HttpGet] [Route("apps/{app}/contributors/")] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] @@ -57,9 +57,9 @@ public sealed class AppContributorsController : ApiController /// /// The name of the app. /// Contributor object that needs to be added to the app. - /// Contributor assigned to app.. - /// Contributor request not valid.. - /// App not found.. + /// Contributor assigned to app. + /// Contributor request not valid. + /// App not found. [HttpPost] [Route("apps/{app}/contributors/")] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status201Created)] @@ -78,8 +78,8 @@ public sealed class AppContributorsController : ApiController /// Remove yourself. /// /// The name of the app. - /// Contributor removed.. - /// Contributor or app not found.. + /// Contributor removed. + /// Contributor or app not found. [HttpDelete] [Route("apps/{app}/contributors/me/")] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] @@ -99,8 +99,8 @@ public sealed class AppContributorsController : ApiController /// /// The name of the app. /// The ID of the contributor. - /// Contributor removed.. - /// Contributor or app not found.. + /// Contributor removed. + /// Contributor or app not found. [HttpDelete] [Route("apps/{app}/contributors/{id}/")] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs index 82f4b7ab9..1e67c827b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs @@ -41,8 +41,8 @@ public sealed class AppImageController : ApiController /// Get the app image. /// /// The name of the app. - /// App image found and content or (resized) image returned.. - /// App not found.. + /// App image found and content or (resized) image returned. + /// App not found. [HttpGet] [Route("apps/{app}/image")] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index b68e2c0d0..8da4b7039 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -32,8 +32,8 @@ public sealed class AppLanguagesController : ApiController /// Get app languages. /// /// The name of the app. - /// Languages returned.. - /// App not found.. + /// Languages returned. + /// App not found. [HttpGet] [Route("apps/{app}/languages/")] [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] @@ -56,9 +56,9 @@ public sealed class AppLanguagesController : ApiController /// /// The name of the app. /// The language to add to the app. - /// Language created.. - /// Language request not valid.. - /// App not found.. + /// Language created. + /// Language request not valid. + /// App not found. [HttpPost] [Route("apps/{app}/languages/")] [ProducesResponseType(typeof(AppLanguagesDto), 201)] @@ -79,9 +79,9 @@ public sealed class AppLanguagesController : ApiController /// The name of the app. /// The language to update. /// The language object. - /// Language updated.. - /// Language request not valid.. - /// Language or app not found.. + /// Language updated. + /// Language request not valid. + /// Language or app not found. [HttpPut] [Route("apps/{app}/languages/{language}/")] [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] @@ -101,9 +101,9 @@ public sealed class AppLanguagesController : ApiController /// /// The name of the app. /// The language to delete from the app. - /// Language deleted.. - /// Language is master language.. - /// Language or app not found.. + /// Language deleted. + /// Language is master language. + /// Language or app not found. [HttpDelete] [Route("apps/{app}/languages/{language}/")] [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs index 7e48adad0..8ffb5351d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs @@ -35,8 +35,8 @@ public sealed class AppRolesController : ApiController /// Get app roles. /// /// The name of the app. - /// Roles returned.. - /// App not found.. + /// Roles returned. + /// App not found. [HttpGet] [Route("apps/{app}/roles/")] [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] @@ -58,8 +58,8 @@ public sealed class AppRolesController : ApiController /// Get app permissions. /// /// The name of the app. - /// App permissions returned.. - /// App not found.. + /// App permissions returned. + /// App not found. [HttpGet] [Route("apps/{app}/roles/permissions")] [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] @@ -82,9 +82,9 @@ public sealed class AppRolesController : ApiController /// /// The name of the app. /// Role object that needs to be added to the app. - /// Role created.. - /// Role request not valid.. - /// App not found.. + /// Role created. + /// Role request not valid. + /// App not found. [HttpPost] [Route("apps/{app}/roles/")] [ProducesResponseType(typeof(RolesDto), 201)] @@ -105,9 +105,9 @@ public sealed class AppRolesController : ApiController /// The name of the app. /// The name of the role to be updated. /// Role to be updated for the app. - /// Role updated.. - /// Role request not valid.. - /// Role or app not found.. + /// Role updated. + /// Role request not valid. + /// Role or app not found. [HttpPut] [Route("apps/{app}/roles/{roleName}/")] [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] @@ -128,9 +128,9 @@ public sealed class AppRolesController : ApiController /// /// The name of the app. /// The name of the role. - /// Role deleted.. - /// Role is in use by contributor or client or a default role.. - /// Role or app not found.. + /// Role deleted. + /// Role is in use by contributor or client or a default role. + /// Role or app not found. [HttpDelete] [Route("apps/{app}/roles/{roleName}/")] [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs index 50a504bcd..25b8081c7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs @@ -29,8 +29,8 @@ public sealed class AppSettingsController : ApiController /// Get the app settings. /// /// The name of the app to get the settings for. - /// App settings returned.. - /// App not found.. + /// App settings returned. + /// App not found. [HttpGet] [Route("apps/{app}/settings")] [ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)] @@ -51,9 +51,9 @@ public sealed class AppSettingsController : ApiController /// /// The name of the app to update. /// The values to update. - /// App updated.. - /// App request not valid.. - /// App not found.. + /// App updated. + /// App request not valid. + /// App not found. [HttpPut] [Route("apps/{app}/settings")] [ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs index 8e3714bd4..2117c3c30 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs @@ -36,8 +36,8 @@ public sealed class AppWorkflowsController : ApiController /// Get app workflow. /// /// The name of the app. - /// Workflows returned.. - /// App not found.. + /// Workflows returned. + /// App not found. [HttpGet] [Route("apps/{app}/workflows/")] [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] @@ -60,9 +60,9 @@ public sealed class AppWorkflowsController : ApiController /// /// The name of the app. /// The new workflow. - /// Workflow created.. - /// Workflow request not valid.. - /// Workflow or app not found.. + /// Workflow created. + /// Workflow request not valid. + /// Workflow or app not found. [HttpPost] [Route("apps/{app}/workflows/")] [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] @@ -83,9 +83,9 @@ public sealed class AppWorkflowsController : ApiController /// The name of the app. /// The ID of the workflow to update. /// The new workflow. - /// Workflow updated.. - /// Workflow request not valid.. - /// Workflow or app not found.. + /// Workflow updated. + /// Workflow request not valid. + /// Workflow or app not found. [HttpPut] [Route("apps/{app}/workflows/{id}")] [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] @@ -105,8 +105,8 @@ public sealed class AppWorkflowsController : ApiController /// /// The name of the app. /// The ID of the workflow to update. - /// Workflow deleted.. - /// Workflow or app not found.. + /// Workflow deleted. + /// Workflow or app not found. [HttpDelete] [Route("apps/{app}/workflows/{id}")] [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index f6a66b6cc..b6ea9ae8c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -37,7 +37,7 @@ public sealed class AppsController : ApiController /// /// Get your apps. /// - /// Apps returned.. + /// Apps returned. /// /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). /// You will retrieve all apps, where you are assigned as a contributor. @@ -68,7 +68,7 @@ public sealed class AppsController : ApiController /// Get team apps. /// /// The ID of the team. - /// Apps returned.. + /// Apps returned. /// /// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow). /// You will retrieve all apps, where you are assigned as a contributor. @@ -96,8 +96,8 @@ public sealed class AppsController : ApiController /// Get an app by name. /// /// The name of the app. - /// Apps returned.. - /// App not found.. + /// Apps returned. + /// App not found. [HttpGet] [Route("apps/{app}")] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] @@ -121,9 +121,9 @@ public sealed class AppsController : ApiController /// Create a new app. /// /// The app object that needs to be added to Squidex. - /// App created.. - /// App request not valid.. - /// App name is already in use.. + /// App created. + /// App request not valid. + /// App name is already in use. /// /// You can only create an app when you are authenticated as a user (OpenID implicit flow). /// You will be assigned as owner of the new app automatically. @@ -145,9 +145,9 @@ public sealed class AppsController : ApiController /// /// The name of the app to update. /// The values to update. - /// App updated.. - /// App request not valid.. - /// App not found.. + /// App updated. + /// App request not valid. + /// App not found. [HttpPut] [Route("apps/{app}/")] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] @@ -165,9 +165,9 @@ public sealed class AppsController : ApiController /// /// The name of the app to update. /// The team information. - /// App transferred.. - /// App request not valid.. - /// App not found.. + /// App transferred. + /// App request not valid. + /// App not found. [HttpPut] [Route("apps/{app}/team")] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] @@ -185,9 +185,9 @@ public sealed class AppsController : ApiController /// /// The name of the app to update. /// The file to upload. - /// App image uploaded.. - /// App request not valid.. - /// App not found.. + /// App image uploaded. + /// App request not valid. + /// App not found. [HttpPost] [Route("apps/{app}/image")] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] @@ -204,8 +204,8 @@ public sealed class AppsController : ApiController /// Remove the app image. /// /// The name of the app to update. - /// App image removed.. - /// App not found.. + /// App image removed. + /// App not found. [HttpDelete] [Route("apps/{app}/image")] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] @@ -222,8 +222,8 @@ public sealed class AppsController : ApiController /// Delete the app. /// /// The name of the app to delete. - /// App deleted.. - /// App not found.. + /// App deleted. + /// App not found. [HttpDelete] [Route("apps/{app}/")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 2867cb83b..aab2d0948 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -52,8 +52,8 @@ public sealed class AssetContentController : ApiController /// The id or slug of the asset. /// The request parameters. /// Optional suffix that can be used to seo-optimize the link to the image Has not effect. - /// Asset found and content or (resized) image returned.. - /// Asset or app not found.. + /// Asset found and content or (resized) image returned. + /// Asset or app not found. [HttpGet] [Route("assets/{app}/{idOrSlug}/{*more}")] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] @@ -79,8 +79,8 @@ public sealed class AssetContentController : ApiController /// /// The ID of the asset. /// The request parameters. - /// Asset found and content or (resized) image returned.. - /// Asset or app not found.. + /// Asset found and content or (resized) image returned. + /// Asset or app not found. [HttpGet] [Route("assets/{id}/")] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs index 27fe42245..cc6db161c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs @@ -39,8 +39,8 @@ public sealed class AssetFoldersController : ApiController /// The name of the app. /// The optional parent folder id. /// The scope of the query. - /// Asset folders returned.. - /// App not found.. + /// Asset folders returned. + /// App not found. /// /// Get all asset folders for the app. /// @@ -90,9 +90,9 @@ public sealed class AssetFoldersController : ApiController /// /// The name of the app. /// The asset folder object that needs to be added to the App. - /// Asset folder created.. - /// Asset folder request not valid.. - /// App not found.. + /// Asset folder created. + /// Asset folder request not valid. + /// App not found. [HttpPost] [Route("apps/{app}/assets/folders", Order = -1)] [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status201Created)] @@ -114,9 +114,9 @@ public sealed class AssetFoldersController : ApiController /// The name of the app. /// The ID of the asset folder. /// The asset folder object that needs to updated. - /// Asset folder updated.. - /// Asset folder request not valid.. - /// Asset folder or app not found.. + /// Asset folder updated. + /// Asset folder request not valid. + /// Asset folder or app not found. [HttpPut] [Route("apps/{app}/assets/folders/{id}/", Order = -1)] [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)] @@ -138,9 +138,9 @@ public sealed class AssetFoldersController : ApiController /// The name of the app. /// The ID of the asset folder. /// The asset folder object that needs to updated. - /// Asset folder moved.. - /// Asset folder request not valid.. - /// Asset folder or app not found.. + /// Asset folder moved. + /// Asset folder request not valid. + /// Asset folder or app not found. [HttpPut] [Route("apps/{app}/assets/folders/{id}/parent", Order = -1)] [ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)] @@ -161,8 +161,8 @@ public sealed class AssetFoldersController : ApiController /// /// The name of the app. /// The ID of the asset folder to delete. - /// Asset folder deleted.. - /// Asset folder or app not found.. + /// Asset folder deleted. + /// Asset folder or app not found. [HttpDelete] [Route("apps/{app}/assets/folders/{id}/", Order = -1)] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index a5879b0db..d262bdbff 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -58,8 +58,8 @@ public sealed class AssetsController : ApiController /// Get assets tags. /// /// The name of the app. - /// Assets tags returned.. - /// App not found.. + /// Assets tags returned. + /// App not found. /// /// Get all tags for assets. /// @@ -83,8 +83,8 @@ public sealed class AssetsController : ApiController /// The name of the app. /// The tag to return. /// The required request object. - /// Asset tag renamed and new tags returned.. - /// App not found.. + /// Asset tag renamed and new tags returned. + /// App not found. [HttpPut] [Route("apps/{app}/assets/tags/{name}")] [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] @@ -104,8 +104,8 @@ public sealed class AssetsController : ApiController /// The optional parent folder id. /// The optional asset ids. /// The optional json query. - /// Assets returned.. - /// App not found.. + /// Assets returned. + /// App not found. /// /// Get all assets for the app. /// @@ -134,8 +134,8 @@ public sealed class AssetsController : ApiController /// /// The name of the app. /// The required query object. - /// Assets returned.. - /// App not found.. + /// Assets returned. + /// App not found. /// /// Get all assets for the app. /// @@ -163,8 +163,8 @@ public sealed class AssetsController : ApiController /// /// The name of the app. /// The ID of the asset to retrieve. - /// Asset found.. - /// Asset or app not found.. + /// Asset found. + /// Asset or app not found. [HttpGet] [Route("apps/{app}/assets/{id}/")] [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] @@ -192,10 +192,10 @@ public sealed class AssetsController : ApiController /// /// The name of the app. /// The request parameters. - /// Asset created.. - /// Asset request not valid.. - /// Asset exceeds the maximum upload size.. - /// App not found.. + /// Asset created. + /// Asset request not valid. + /// Asset exceeds the maximum upload size. + /// App not found. /// /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. /// @@ -218,10 +218,10 @@ public sealed class AssetsController : ApiController /// Upload a new asset using tus.io. /// /// The name of the app. - /// Asset created.. - /// Asset request not valid.. - /// Asset exceeds the maximum upload size.. - /// App not found.. + /// Asset created. + /// Asset request not valid. + /// Asset exceeds the maximum upload size. + /// App not found. /// /// Use the tus protocol to upload an asset. /// @@ -254,9 +254,9 @@ public sealed class AssetsController : ApiController /// /// The name of the app. /// The bulk update request. - /// Assets created, update or delete.. - /// Assets request not valid.. - /// App not found.. + /// Assets created, update or delete. + /// Assets request not valid. + /// App not found. [HttpPost] [Route("apps/{app}/assets/bulk")] [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] @@ -280,10 +280,10 @@ public sealed class AssetsController : ApiController /// The name of the app. /// The optional custom asset id. /// The request parameters. - /// Asset created or updated.. - /// Asset request not valid.. - /// Asset exceeds the maximum upload size.. - /// App not found.. + /// Asset created or updated. + /// Asset request not valid. + /// Asset exceeds the maximum upload size. + /// App not found. /// /// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly. /// @@ -308,10 +308,10 @@ public sealed class AssetsController : ApiController /// The name of the app. /// The ID of the asset. /// The file to upload. - /// Asset updated.. - /// Asset request not valid.. - /// Asset exceeds the maximum upload size.. - /// Asset or app not found.. + /// Asset updated. + /// Asset request not valid. + /// Asset exceeds the maximum upload size. + /// Asset or app not found. /// /// Use multipart request to upload an asset. /// @@ -336,9 +336,9 @@ public sealed class AssetsController : ApiController /// The name of the app. /// The ID of the asset. /// The asset object that needs to updated. - /// Asset updated.. - /// Asset request not valid.. - /// Asset or app not found.. + /// Asset updated. + /// Asset request not valid. + /// Asset or app not found. [HttpPut] [Route("apps/{app}/assets/{id}/")] [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] @@ -360,9 +360,9 @@ public sealed class AssetsController : ApiController /// The name of the app. /// The ID of the asset. /// The asset object that needs to updated. - /// Asset moved.. - /// Asset request not valid.. - /// Asset or app not found.. + /// Asset moved. + /// Asset request not valid. + /// Asset or app not found. [HttpPut] [Route("apps/{app}/assets/{id}/parent")] [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] @@ -384,8 +384,8 @@ public sealed class AssetsController : ApiController /// The name of the app. /// The ID of the asset to delete. /// The request parameters. - /// Asset deleted.. - /// Asset or app not found.. + /// Asset deleted. + /// Asset or app not found. [HttpDelete] [Route("apps/{app}/assets/{id}/")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs index 77cb1644f..15199d298 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs @@ -37,8 +37,8 @@ public class BackupContentController : ApiController /// /// The name of the app. /// The ID of the backup. - /// Backup found and content returned.. - /// Backup or app not found.. + /// Backup found and content returned. + /// Backup or app not found. [HttpGet] [Route("apps/{app}/backups/{id}")] [ResponseCache(Duration = 3600 * 24 * 30)] @@ -56,8 +56,8 @@ public class BackupContentController : ApiController /// The ID of the backup. /// The ID of the app. /// The name of the app. - /// Backup found and content returned.. - /// Backup or app not found.. + /// Backup found and content returned. + /// Backup or app not found. [HttpGet] [Route("apps/backups/{id}")] [ResponseCache(Duration = 3600 * 24 * 30)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs index 080611e0e..75b7d9eb6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs @@ -34,8 +34,8 @@ public class BackupsController : ApiController /// Get all backup jobs. /// /// The name of the app. - /// Backups returned.. - /// App not found.. + /// Backups returned. + /// App not found. [HttpGet] [Route("apps/{app}/backups/")] [ProducesResponseType(typeof(BackupJobsDto), StatusCodes.Status200OK)] @@ -54,9 +54,9 @@ public class BackupsController : ApiController /// Start a new backup. /// /// The name of the app. - /// Backup started.. - /// Backup contingent reached.. - /// App not found.. + /// Backup started. + /// Backup contingent reached. + /// App not found. [HttpPost] [Route("apps/{app}/backups/")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -74,8 +74,8 @@ public class BackupsController : ApiController /// /// The name of the app. /// The ID of the backup to delete. - /// Backup deleted.. - /// Backup or app not found.. + /// Backup deleted. + /// Backup or app not found. [HttpDelete] [Route("apps/{app}/backups/{id}")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs index 09bf5ad81..dd93f220e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs @@ -33,7 +33,7 @@ public class RestoreController : ApiController /// /// Get current restore status. /// - /// Status returned.. + /// Status returned. [HttpGet] [Route("apps/restore/")] [ProducesResponseType(typeof(RestoreJobDto), StatusCodes.Status200OK)] @@ -56,7 +56,7 @@ public class RestoreController : ApiController /// Restore a backup. /// /// The backup to restore. - /// Restore operation started.. + /// Restore operation started. [HttpPost] [Route("apps/restore/")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs index 4e3be2eff..e61b61d0d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs @@ -5,162 +5,40 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Globalization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Comments.Models; -using Squidex.Domain.Apps.Entities.Comments; -using Squidex.Domain.Apps.Entities.Comments.Commands; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Collaboration; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Security; using Squidex.Shared; using Squidex.Web; +using YDotNet.Server.WebSockets; namespace Squidex.Areas.Api.Controllers.Comments; -/// -/// Update and query comments for any kind of app resource. -/// -[ApiExplorerSettings(GroupName = nameof(Comments))] public sealed class CommentsController : ApiController { - private readonly ICommentsLoader commentsLoader; - private readonly IWatchingService watchingService; + private readonly ICollaborationService collaboration; - public CommentsController(ICommandBus commandBus, ICommentsLoader commentsLoader, - IWatchingService watchingService) + public CommentsController(ICommandBus commandBus, ICollaborationService collaboration) : base(commandBus) { - this.commentsLoader = commentsLoader; - this.watchingService = watchingService; + this.collaboration = collaboration; } - /// - /// Get all watching users.. - /// - /// The name of the app. - /// The path to the resource. - /// Watching users returned.. - /// App not found.. - [HttpGet] - [Route("apps/{app}/watching/{*resource}")] - [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] - [ApiPermissionOrAnonymous] - [ApiCosts(0)] - public async Task GetWatchingUsers(string app, string? resource = null) + [Route("users/collaboration")] + [ApiPermission] + public IActionResult UserDocument() { - var result = await watchingService.GetWatchingUsersAsync(App.Id, resource, UserId, HttpContext.RequestAborted); - - return Ok(result); + return new YDotNetActionResult(collaboration.UserDocument(User.UserOrClientId()!)); } - /// - /// Get all comments. - /// - /// The name of the app. - /// The ID of the comments. - /// The current version. - /// - /// When passing in a version you can retrieve all updates since then. - /// - /// Comments returned.. - /// App not found.. - [HttpGet] - [Route("apps/{app}/comments/{commentsId}")] - [ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)] + [Route("apps/{app}/collaboration/{commentsId}")] [ApiPermissionOrAnonymous(PermissionIds.AppCommentsRead)] [ApiCosts(0)] - public async Task GetComments(string app, DomainId commentsId, [FromQuery] long version = EtagVersion.Any) - { - var result = await commentsLoader.GetCommentsAsync(Id(commentsId), version, HttpContext.RequestAborted); - - var response = Deferred.Response(() => - { - return CommentsDto.FromDomain(result); - }); - - Response.Headers[HeaderNames.ETag] = result.Version.ToString(CultureInfo.InvariantCulture); - - return Ok(response); - } - - /// - /// Create a new comment. - /// - /// The name of the app. - /// The ID of the comments. - /// The comment object that needs to created. - /// Comment created.. - /// Comment request not valid.. - /// App not found.. - [HttpPost] - [Route("apps/{app}/comments/{commentsId}")] - [ProducesResponseType(typeof(CommentDto), 201)] - [ApiPermissionOrAnonymous(PermissionIds.AppCommentsCreate)] - [ApiCosts(0)] - public async Task PostComment(string app, DomainId commentsId, [FromBody] UpsertCommentDto request) - { - var command = request.ToCreateCommand(commentsId); - - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - - var response = CommentDto.FromDomain(command); - - return CreatedAtAction(nameof(GetComments), new { app, commentsId }, response); - } - - /// - /// Update a comment. - /// - /// The name of the app. - /// The ID of the comments. - /// The ID of the comment. - /// The comment object that needs to updated. - /// Comment updated.. - /// Comment request not valid.. - /// Comment or app not found.. - [HttpPut] - [Route("apps/{app}/comments/{commentsId}/{commentId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ApiPermissionOrAnonymous(PermissionIds.AppCommentsUpdate)] - [ApiCosts(0)] - public async Task PutComment(string app, DomainId commentsId, DomainId commentId, [FromBody] UpsertCommentDto request) - { - var command = request.ToUpdateComment(commentsId, commentId); - - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - - return NoContent(); - } - - /// - /// Delete a comment. - /// - /// The name of the app. - /// The ID of the comments. - /// The ID of the comment. - /// Comment deleted.. - /// Comment or app not found.. - [HttpDelete] - [Route("apps/{app}/comments/{commentsId}/{commentId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ApiPermissionOrAnonymous(PermissionIds.AppCommentsDelete)] - [ApiCosts(0)] - public async Task DeleteComment(string app, DomainId commentsId, DomainId commentId) - { - var command = new DeleteComment - { - CommentsId = commentsId, - CommentId = commentId - }; - - await CommandBus.PublishAsync(command, HttpContext.RequestAborted); - - return NoContent(); - } - - private DomainId Id(DomainId commentsId) + public IActionResult CollaborationDocument(string app, DomainId commentsId) { - return DomainId.Combine(App.Id, commentsId); + return new YDotNetActionResult(collaboration.ResourceDocument(App.NamedId(), commentsId)); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs deleted file mode 100644 index bece5d04b..000000000 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Core.Comments; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Areas.Api.Controllers.Comments.Models; - -public sealed class CommentDto -{ - /// - /// The ID of the comment. - /// - public DomainId Id { get; set; } - - /// - /// The time when the comment was created or updated last. - /// - public Instant Time { get; set; } - - /// - /// The user who created or updated the comment. - /// - public RefToken User { get; set; } - - /// - /// The text of the comment. - /// - public string Text { get; set; } - - /// - /// The url where the comment is created. - /// - public Uri? Url { get; set; } - - public static CommentDto FromDomain(Comment comment) - { - var result = SimpleMapper.Map(comment, new CommentDto()); - - return result; - } - - public static CommentDto FromDomain(CreateComment command) - { - var time = SystemClock.Instance.GetCurrentInstant(); - - return SimpleMapper.Map(command, new CommentDto { Id = command.CommentId, User = command.Actor, Time = time }); - } -} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs deleted file mode 100644 index e2046e616..000000000 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Comments; -using Squidex.Infrastructure; - -namespace Squidex.Areas.Api.Controllers.Comments.Models; - -public sealed class CommentsDto -{ - /// - /// The created comments including the updates. - /// - public CommentDto[]? CreatedComments { get; set; } - - /// - /// The updates comments since the last version. - /// - public CommentDto[]? UpdatedComments { get; set; } - - /// - /// The deleted comments since the last version. - /// - public List? DeletedComments { get; set; } - - /// - /// The current version. - /// - public long Version { get; set; } - - public static CommentsDto FromDomain(CommentsResult comments) - { - var result = new CommentsDto - { - CreatedComments = comments.CreatedComments.Select(CommentDto.FromDomain).ToArray(), - UpdatedComments = comments.UpdatedComments.Select(CommentDto.FromDomain).ToArray(), - DeletedComments = comments.DeletedComments, - Version = comments.Version - }; - - return result; - } -} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs deleted file mode 100644 index 0dfa2d6e9..000000000 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Validation; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Comments.Models; - -[OpenApiRequest] -public sealed class UpsertCommentDto -{ - /// - /// The comment text. - /// - [LocalizedRequired] - public string Text { get; set; } - - /// - /// The url where the comment is created. - /// - public Uri? Url { get; set; } - - public CreateComment ToCreateCommand(DomainId commentsId) - { - return SimpleMapper.Map(this, new CreateComment - { - CommentsId = commentsId - }); - } - - public UpdateComment ToUpdateComment(DomainId commentsId, DomainId commentId) - { - return SimpleMapper.Map(this, new UpdateComment - { - CommentsId = commentsId, - CommentId = commentId - }); - } -} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs deleted file mode 100644 index bb07151b9..000000000 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs +++ /dev/null @@ -1,99 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Globalization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using Squidex.Areas.Api.Controllers.Comments.Models; -using Squidex.Domain.Apps.Entities.Comments; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Translations; -using Squidex.Web; - -namespace Squidex.Areas.Api.Controllers.Comments.Notifications; - -/// -/// Update and query user notifications. -/// -[ApiExplorerSettings(GroupName = nameof(Notifications))] -public sealed class UserNotificationsController : ApiController -{ - private readonly ICommentsLoader commentsLoader; - - public UserNotificationsController(ICommandBus commandBus, ICommentsLoader commentsLoader) - : base(commandBus) - { - this.commentsLoader = commentsLoader; - } - - /// - /// Get all notifications. - /// - /// The user id. - /// The current version. - /// - /// When passing in a version you can retrieve all updates since then. - /// - /// All comments returned.. - [HttpGet] - [Route("users/{userId}/notifications")] - [ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)] - [ApiPermission] - public async Task GetNotifications(DomainId userId, [FromQuery] long version = EtagVersion.Any) - { - CheckPermissions(userId); - - var result = await commentsLoader.GetCommentsAsync(userId, version, HttpContext.RequestAborted); - - var response = Deferred.Response(() => - { - return CommentsDto.FromDomain(result); - }); - - Response.Headers[HeaderNames.ETag] = result.Version.ToString(CultureInfo.InvariantCulture); - - return Ok(response); - } - - /// - /// Delete a notification. - /// - /// The user id. - /// The ID of the comment. - /// Comment deleted.. - /// Comment not found.. - [HttpDelete] - [Route("users/{userId}/notifications/{commentId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ApiPermission] - public async Task DeleteComment(DomainId userId, DomainId commentId) - { - CheckPermissions(userId); - - var commmand = new DeleteComment - { - AppId = CommentsCommand.NoApp, - CommentsId = userId, - CommentId = commentId - }; - - await CommandBus.PublishAsync(commmand, HttpContext.RequestAborted); - - return NoContent(); - } - - private void CheckPermissions(DomainId userId) - { - if (!string.Equals(userId.ToString(), User.OpenIdSubject(), StringComparison.Ordinal)) - { - throw new DomainForbiddenException(T.Get("comments.noPermissions")); - } - } -} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index f508d3052..8b83ded1b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -43,8 +43,8 @@ public sealed class ContentsController : ApiController /// The name of the app. /// The name of the schema. /// The number of items to skip. - /// Contents returned.. - /// Schema or app not found.. + /// Contents returned. + /// Schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -68,8 +68,8 @@ public sealed class ContentsController : ApiController /// The name of the schema. /// The optional ids of the content to fetch. /// The optional json query. - /// Contents returned.. - /// Schema or app not found.. + /// Contents returned. + /// Schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -103,8 +103,8 @@ public sealed class ContentsController : ApiController /// The name of the app. /// The name of the schema. /// The required query object. - /// Contents returned.. - /// Schema or app not found.. + /// Contents returned. + /// Schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -138,8 +138,8 @@ public sealed class ContentsController : ApiController /// The name of the schema. /// The ID of the content to fetch. /// The optional version. - /// Content returned.. - /// Content, schema or app not found.. + /// Content returned. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -175,9 +175,9 @@ public sealed class ContentsController : ApiController /// The name of the app. /// The name of the schema. /// The ID of the content to fetch. - /// Content is valid.. - /// Content not valid.. - /// Content, schema or app not found.. + /// Content is valid. + /// Content not valid. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -202,8 +202,8 @@ public sealed class ContentsController : ApiController /// The name of the schema. /// The ID of the content to fetch. /// The optional json query. - /// Contents returned.. - /// Content, schema or app not found.. + /// Contents returned. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -237,8 +237,8 @@ public sealed class ContentsController : ApiController /// The name of the schema. /// The ID of the content to fetch. /// The optional json query. - /// Content returned.. - /// Content, schema or app not found.. + /// Content returned. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -272,8 +272,8 @@ public sealed class ContentsController : ApiController /// The name of the schema. /// The ID of the content to fetch. /// The version fo the content to fetch. - /// Content version returned.. - /// Content, schema or app not found.. + /// Content version returned. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -307,9 +307,9 @@ public sealed class ContentsController : ApiController /// The name of the app. /// The name of the schema. /// The request parameters. - /// Content created.. - /// Content request not valid.. - /// Content, schema or app not found.. + /// Content created. + /// Content request not valid. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -335,9 +335,9 @@ public sealed class ContentsController : ApiController /// The name of the app. /// The name of the schema. /// The import request. - /// Contents created.. - /// Content request not valid.. - /// Content references, schema or app not found.. + /// Contents created. + /// Content request not valid. + /// Content references, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -365,9 +365,9 @@ public sealed class ContentsController : ApiController /// The name of the app. /// The name of the schema. /// The bulk update request. - /// Contents created, update or delete.. - /// Contents request not valid.. - /// Contents references, schema or app not found.. + /// Contents created, update or delete. + /// Contents request not valid. + /// Contents references, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -395,9 +395,9 @@ public sealed class ContentsController : ApiController /// The name of the schema. /// The ID of the content item to update. /// The request parameters. - /// Content created or updated.. - /// Content request not valid.. - /// Content references, schema or app not found.. + /// Content created or updated. + /// Content request not valid. + /// Content references, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -424,9 +424,9 @@ public sealed class ContentsController : ApiController /// The name of the schema. /// The ID of the content item to update. /// The full data for the content item. - /// Content updated.. - /// Content request not valid.. - /// Content references, schema or app not found.. + /// Content updated. + /// Content request not valid. + /// Content references, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -453,9 +453,9 @@ public sealed class ContentsController : ApiController /// The name of the schema. /// The ID of the content item to patch. /// The patch for the content item. - /// Content patched.. - /// Content request not valid.. - /// Content, schema or app not found.. + /// Content patched. + /// Content request not valid. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -482,9 +482,9 @@ public sealed class ContentsController : ApiController /// The name of the schema. /// The ID of the content item to change. /// The status request. - /// Content status changed.. - /// Content request not valid.. - /// Content, schema or app not found.. + /// Content status changed. + /// Content request not valid. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -510,9 +510,9 @@ public sealed class ContentsController : ApiController /// The name of the app. /// The name of the schema. /// The ID of the content item to cancel. - /// Content status change cancelled.. - /// Content request not valid.. - /// Content, schema or app not found.. + /// Content status change cancelled. + /// Content request not valid. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -538,8 +538,8 @@ public sealed class ContentsController : ApiController /// The name of the app. /// The name of the schema. /// The ID of the content item to create the draft for. - /// Content draft created.. - /// Content, schema or app not found.. + /// Content draft created. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -565,8 +565,8 @@ public sealed class ContentsController : ApiController /// The name of the app. /// The name of the schema. /// The ID of the content item to delete the draft from. - /// Content draft deleted.. - /// Content, schema or app not found.. + /// Content draft deleted. + /// Content, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -593,9 +593,9 @@ public sealed class ContentsController : ApiController /// The name of the schema. /// The ID of the content item to delete. /// The request parameters. - /// Content deleted.. - /// Content cannot be deleted.. - /// Content, schema or app not found.. + /// Content deleted. + /// Content cannot be deleted. + /// Content, schema or app not found. /// /// You can create an generated documentation for your app at /api/content/{appName}/docs. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs index 3fd095063..32b219f30 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs @@ -41,8 +41,8 @@ public sealed class ContentsSharedController : ApiController /// /// The name of the app. /// The request parameters. - /// Contents returned or mutated.. - /// App not found.. + /// Contents returned or mutated. + /// App not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -66,8 +66,8 @@ public sealed class ContentsSharedController : ApiController /// GraphQL endpoint. /// /// The name of the app. - /// Contents returned or mutated.. - /// App not found.. + /// Contents returned or mutated. + /// App not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -93,8 +93,8 @@ public sealed class ContentsSharedController : ApiController /// /// The name of the app. /// The request object. - /// Contents returned or mutated.. - /// App not found.. + /// Contents returned or mutated. + /// App not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -118,8 +118,8 @@ public sealed class ContentsSharedController : ApiController /// GraphQL batch endpoint. /// /// The name of the app. - /// Contents returned or mutated.. - /// App not found.. + /// Contents returned or mutated. + /// App not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -145,8 +145,8 @@ public sealed class ContentsSharedController : ApiController /// /// The name of the app. /// The query object. - /// Contents returned.. - /// App not found.. + /// Contents returned. + /// App not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -178,8 +178,8 @@ public sealed class ContentsSharedController : ApiController /// /// The name of the app. /// The required query object. - /// Contents returned.. - /// App not found.. + /// Contents returned. + /// App not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// @@ -212,9 +212,9 @@ public sealed class ContentsSharedController : ApiController /// The name of the app. /// The name of the schema. /// The bulk update request. - /// Contents created, update or delete.. - /// Contents request not valid.. - /// Contents references, schema or app not found.. + /// Contents created, update or delete. + /// Contents request not valid. + /// Contents references, schema or app not found. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs index 371a0c185..a91cb1ffa 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs @@ -30,8 +30,8 @@ public sealed class DiagnosticsController : ApiController /// /// Creates a dump and writes it into storage.. /// - /// Dump created successful.. - /// Not configured.. + /// Dump created successful. + /// Not configured. [HttpGet] [Route("diagnostics/dump")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -51,8 +51,8 @@ public sealed class DiagnosticsController : ApiController /// /// Creates a gc dump and writes it into storage. /// - /// Dump created successful.. - /// Not configured.. + /// Dump created successful. + /// Not configured. [HttpGet] [Route("diagnostics/gcdump")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs index df5058cb9..3017d42be 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs @@ -31,7 +31,7 @@ public sealed class EventConsumersController : ApiController /// /// Get event consumers. /// - /// Event consumers returned.. + /// Event consumers returned. [HttpGet] [Route("event-consumers/")] [ProducesResponseType(typeof(EventConsumersDto), StatusCodes.Status200OK)] @@ -49,8 +49,8 @@ public sealed class EventConsumersController : ApiController /// Start an event consumer. /// /// The name of the event consumer. - /// Event consumer started asynchronously.. - /// Event consumer not found.. + /// Event consumer started asynchronously. + /// Event consumer not found. [HttpPut] [Route("event-consumers/{consumerName}/start/")] [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] @@ -68,8 +68,8 @@ public sealed class EventConsumersController : ApiController /// Stop an event consumer. /// /// The name of the event consumer. - /// Event consumer stopped asynchronously.. - /// Event consumer not found.. + /// Event consumer stopped asynchronously. + /// Event consumer not found. [HttpPut] [Route("event-consumers/{consumerName}/stop/")] [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] @@ -87,8 +87,8 @@ public sealed class EventConsumersController : ApiController /// Reset an event consumer. /// /// The name of the event consumer. - /// Event consumer resetted asynchronously.. - /// Event consumer not found.. + /// Event consumer resetted asynchronously. + /// Event consumer not found. [HttpPut] [Route("event-consumers/{consumerName}/reset/")] [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index 675078c98..5be7085bd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -33,8 +33,8 @@ public sealed class HistoryController : ApiController /// /// The name of the app. /// The name of the channel. - /// Events returned.. - /// App not found.. + /// Events returned. + /// App not found. [HttpGet] [Route("apps/{app}/history/")] [ProducesResponseType(typeof(HistoryEventDto[]), StatusCodes.Status200OK)] @@ -54,8 +54,8 @@ public sealed class HistoryController : ApiController /// /// The ID of the team. /// The name of the channel. - /// Events returned.. - /// Team not found.. + /// Events returned. + /// Team not found. [HttpGet] [Route("teams/{team}/history/")] [ProducesResponseType(typeof(HistoryEventDto[]), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs index fd30e5e80..99343debd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs @@ -30,7 +30,7 @@ public sealed class LanguagesController : ApiController /// /// Provide a list of supported language codes, following the ISO2Code standard. /// - /// Supported language codes returned.. + /// Supported language codes returned. [HttpGet] [Route("languages/")] [ProducesResponseType(typeof(LanguageDto[]), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs b/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs index 01a827364..f9780725a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs @@ -31,7 +31,7 @@ public sealed class NewsController : ApiController /// Get features since version. /// /// The latest received version. - /// Latest features returned.. + /// Latest features returned. [HttpGet] [Route("news/features/")] [ProducesResponseType(typeof(FeaturesDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs index feb38ba74..e45d4a759 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs @@ -29,7 +29,7 @@ public sealed class PingController : ApiController /// /// Get API information. /// - /// Infos returned.. + /// Infos returned. [HttpGet] [Route("info/")] [ProducesResponseType(typeof(ExposedValues), StatusCodes.Status200OK)] @@ -41,7 +41,7 @@ public sealed class PingController : ApiController /// /// Get ping status of the API. /// - /// Service ping successful.. + /// Service ping successful. /// /// Can be used to test, if the Squidex API is alive and responding. /// @@ -57,7 +57,7 @@ public sealed class PingController : ApiController /// Get ping status. /// /// The name of the app. - /// Service ping successful.. + /// Service ping successful. /// /// Can be used to test, if the Squidex API is alive and responding. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index 8f8335ec8..cd16967f8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -43,8 +43,8 @@ public sealed class AppPlansController : ApiController /// Get app plan information. /// /// The name of the app. - /// App plan information returned.. - /// App not found.. + /// App plan information returned. + /// App not found. [HttpGet] [Route("apps/{app}/plans/")] [ProducesResponseType(typeof(PlansDto), StatusCodes.Status200OK)] @@ -101,9 +101,9 @@ public sealed class AppPlansController : ApiController /// /// The name of the app. /// Plan object that needs to be changed. - /// Plan changed or redirect url returned.. - /// Plan not owned by user.. - /// App not found.. + /// Plan changed or redirect url returned. + /// Plan not owned by user. + /// App not found. [HttpPut] [Route("apps/{app}/plan/")] [ProducesResponseType(typeof(PlanChangedDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs index 3cacfe48d..8985a15f8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/TeamPlansController.cs @@ -43,8 +43,8 @@ public sealed class TeamPlansController : ApiController /// Get team plan information. /// /// The name of the team. - /// Team plan information returned.. - /// Team not found.. + /// Team plan information returned. + /// Team not found. [HttpGet] [Route("teams/{team}/plans/")] [ProducesResponseType(typeof(PlansDto), StatusCodes.Status200OK)] @@ -90,8 +90,8 @@ public sealed class TeamPlansController : ApiController /// /// The name of the team. /// Plan object that needs to be changed. - /// Plan changed or redirect url returned.. - /// Team not found.. + /// Plan changed or redirect url returned. + /// Team not found. [HttpPut] [Route("teams/{team}/plan/")] [ProducesResponseType(typeof(PlanChangedDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 767d3823d..37df73f64 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -58,7 +58,7 @@ public sealed class RulesController : ApiController /// /// Get supported rule actions. /// - /// Rule actions returned.. + /// Rule actions returned. [HttpGet] [Route("rules/actions/")] [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] @@ -82,8 +82,8 @@ public sealed class RulesController : ApiController /// Get rules. /// /// The name of the app. - /// Rules returned.. - /// App not found.. + /// Rules returned. + /// App not found. [HttpGet] [Route("apps/{app}/rules/")] [ProducesResponseType(typeof(RulesDto), StatusCodes.Status200OK)] @@ -106,9 +106,9 @@ public sealed class RulesController : ApiController /// /// The name of the app. /// The rule object that needs to be added to the app. - /// Rule created.. - /// Rule request not valid.. - /// App not found.. + /// Rule created. + /// Rule request not valid. + /// App not found. [HttpPost] [Route("apps/{app}/rules/")] [ProducesResponseType(typeof(RuleDto), 201)] @@ -127,7 +127,7 @@ public sealed class RulesController : ApiController /// Cancel the current run. /// /// The name of the app. - /// Rule run cancelled.. + /// Rule run cancelled. [HttpDelete] [Route("apps/{app}/rules/run")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -146,9 +146,9 @@ public sealed class RulesController : ApiController /// The name of the app. /// The ID of the rule to update. /// The rule object that needs to be added to the app. - /// Rule updated.. - /// Rule request not valid.. - /// Rule or app not found.. + /// Rule updated. + /// Rule request not valid. + /// Rule or app not found. [HttpPut] [Route("apps/{app}/rules/{id}/")] [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] @@ -168,8 +168,8 @@ public sealed class RulesController : ApiController /// /// The name of the app. /// The ID of the rule to enable. - /// Rule enabled.. - /// Rule or app not found.. + /// Rule enabled. + /// Rule or app not found. [HttpPut] [Route("apps/{app}/rules/{id}/enable/")] [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] @@ -189,8 +189,8 @@ public sealed class RulesController : ApiController /// /// The name of the app. /// The ID of the rule to disable. - /// Rule disabled.. - /// Rule or app not found.. + /// Rule disabled. + /// Rule or app not found. [HttpPut] [Route("apps/{app}/rules/{id}/disable/")] [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] @@ -210,8 +210,8 @@ public sealed class RulesController : ApiController /// /// The name of the app. /// The ID of the rule to disable. - /// Rule triggered.. - /// Rule or app not found.. + /// Rule triggered. + /// Rule or app not found. [HttpPut] [Route("apps/{app}/rules/{id}/trigger/")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -232,7 +232,7 @@ public sealed class RulesController : ApiController /// The name of the app. /// The ID of the rule to run. /// Runs the rule from snapeshots if possible. - /// Rule started.. + /// Rule started. [HttpPut] [Route("apps/{app}/rules/{id}/run")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -250,7 +250,7 @@ public sealed class RulesController : ApiController /// /// The name of the app. /// The ID of the rule to cancel. - /// Rule events cancelled.. + /// Rule events cancelled. [HttpDelete] [Route("apps/{app}/rules/{id}/events/")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -268,8 +268,8 @@ public sealed class RulesController : ApiController /// /// The name of the app. /// The rule to simulate. - /// Rule simulated.. - /// Rule or app not found.. + /// Rule simulated. + /// Rule or app not found. [HttpPost] [Route("apps/{app}/rules/simulate/")] [ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)] @@ -291,8 +291,8 @@ public sealed class RulesController : ApiController /// /// The name of the app. /// The ID of the rule to simulate. - /// Rule simulated.. - /// Rule or app not found.. + /// Rule simulated. + /// Rule or app not found. [HttpGet] [Route("apps/{app}/rules/{id}/simulate/")] [ProducesResponseType(typeof(SimulatedRuleEventsDto), StatusCodes.Status200OK)] @@ -319,8 +319,8 @@ public sealed class RulesController : ApiController /// /// The name of the app. /// The ID of the rule to delete. - /// Rule deleted.. - /// Rule or app not found.. + /// Rule deleted. + /// Rule or app not found. [HttpDelete] [Route("apps/{app}/rules/{id}/")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -342,8 +342,8 @@ public sealed class RulesController : ApiController /// The optional rule id to filter to events. /// The number of events to skip. /// The number of events to take. - /// Rule events returned.. - /// App not found.. + /// Rule events returned. + /// App not found. [HttpGet] [Route("apps/{app}/rules/events/")] [ProducesResponseType(typeof(RuleEventsDto), StatusCodes.Status200OK)] @@ -363,8 +363,8 @@ public sealed class RulesController : ApiController /// /// The name of the app. /// The event to enqueue. - /// Rule enqueued.. - /// App or rule event not found.. + /// Rule enqueued. + /// App or rule event not found. [HttpPut] [Route("apps/{app}/rules/events/{id}/")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -389,8 +389,8 @@ public sealed class RulesController : ApiController /// /// The name of the app. /// The event to cancel. - /// Rule event cancelled.. - /// App or rule event not found.. + /// Rule event cancelled. + /// App or rule event not found. [HttpDelete] [Route("apps/{app}/rules/events/{id}/")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -414,7 +414,7 @@ public sealed class RulesController : ApiController /// Cancels all events. /// /// The name of the app. - /// Events cancelled.. + /// Events cancelled. [HttpDelete] [Route("apps/{app}/rules/events/")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -430,7 +430,7 @@ public sealed class RulesController : ApiController /// /// Provide a list of all event types that are used in rules. /// - /// Rule events returned.. + /// Rule events returned. [HttpGet] [Route("rules/eventtypes")] [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] @@ -446,8 +446,8 @@ public sealed class RulesController : ApiController /// Provide the json schema for the event with the specified name. /// /// The type name of the event. - /// Rule event type found.. - /// Rule event not found.. + /// Rule event type found. + /// Rule event not found. [HttpGet] [Route("rules/eventtypes/{type}")] [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index 0cb24c17d..3e0f1f29b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -32,10 +32,10 @@ public sealed class SchemaFieldsController : ApiController /// The name of the app. /// The name of the schema. /// The field object that needs to be added to the schema. - /// Schema field created.. - /// Schema request not valid.. - /// Schema or app not found.. - /// Schema field name already in use.. + /// Schema field created. + /// Schema request not valid. + /// Schema or app not found. + /// Schema field name already in use. [HttpPost] [Route("apps/{app}/schemas/{schema}/fields/")] [ProducesResponseType(typeof(SchemaDto), 201)] @@ -57,10 +57,10 @@ public sealed class SchemaFieldsController : ApiController /// The name of the schema. /// The parent field id. /// The field object that needs to be added to the schema. - /// Schema field created.. - /// Schema request not valid.. - /// Schema field name already in use.. - /// Schema, field or app not found.. + /// Schema field created. + /// Schema request not valid. + /// Schema field name already in use. + /// Schema, field or app not found. [HttpPost] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/")] [ProducesResponseType(typeof(SchemaDto), 201)] @@ -81,9 +81,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the app. /// The name of the schema. /// The request that contains the field names. - /// Schema UI fields defined.. - /// Schema request not valid.. - /// Schema or app not found.. + /// Schema UI fields defined. + /// Schema request not valid. + /// Schema or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/fields/ui/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -104,9 +104,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the app. /// The name of the schema. /// The request that contains the field ids. - /// Schema fields reordered.. - /// Schema request not valid.. - /// Schema or app not found.. + /// Schema fields reordered. + /// Schema request not valid. + /// Schema or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/fields/ordering/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -128,9 +128,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the schema. /// The parent field id. /// The request that contains the field ids. - /// Schema fields reordered.. - /// Schema field request not valid.. - /// Schema, field or app not found.. + /// Schema fields reordered. + /// Schema field request not valid. + /// Schema, field or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/ordering/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -152,9 +152,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the schema. /// The ID of the field to update. /// The field object that needs to be added to the schema. - /// Schema field updated.. - /// Schema field request not valid.. - /// Schema, field or app not found.. + /// Schema field updated. + /// Schema field request not valid. + /// Schema, field or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/fields/{id:long}/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -177,9 +177,9 @@ public sealed class SchemaFieldsController : ApiController /// The parent field id. /// The ID of the field to update. /// The field object that needs to be added to the schema. - /// Schema field updated.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field updated. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -200,9 +200,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the app. /// The name of the schema. /// The ID of the field to lock. - /// Schema field shown.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field shown. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. /// /// A locked field cannot be updated or deleted. /// @@ -227,9 +227,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the schema. /// The parent field id. /// The ID of the field to lock. - /// Schema field hidden.. - /// Schema field request not valid or field locked.. - /// Field, schema, or app not found.. + /// Schema field hidden. + /// Schema field request not valid or field locked. + /// Field, schema, or app not found. /// /// A locked field cannot be edited or deleted. /// @@ -253,9 +253,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the app. /// The name of the schema. /// The ID of the field to hide. - /// Schema field hidden.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field hidden. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. /// /// A hidden field is not part of the API response, but can still be edited in the portal. /// @@ -280,9 +280,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the schema. /// The parent field id. /// The ID of the field to hide. - /// Schema field hidden.. - /// Schema field request not valid or field locked.. - /// Field, schema, or app not found.. + /// Schema field hidden. + /// Schema field request not valid or field locked. + /// Field, schema, or app not found. /// /// A hidden field is not part of the API response, but can still be edited in the portal. /// @@ -306,9 +306,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the app. /// The name of the schema. /// The ID of the field to show. - /// Schema field shown.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field shown. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. /// /// A hidden field is not part of the API response, but can still be edited in the portal. /// @@ -333,9 +333,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the schema. /// The parent field id. /// The ID of the field to show. - /// Schema field shown.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field shown. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. /// /// A hidden field is not part of the API response, but can still be edited in the portal. /// @@ -359,9 +359,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the app. /// The name of the schema. /// The ID of the field to enable. - /// Schema field enabled.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field enabled. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. /// /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. /// @@ -386,9 +386,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the schema. /// The parent field id. /// The ID of the field to enable. - /// Schema field enabled.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field enabled. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. /// /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. /// @@ -412,9 +412,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the app. /// The name of the schema. /// The ID of the field to disable. - /// Schema field disabled.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field disabled. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. /// /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. /// @@ -439,9 +439,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the schema. /// The parent field id. /// The ID of the field to disable. - /// Schema field disabled.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field disabled. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. /// /// A disabled field cannot not be edited in the squidex portal anymore, but will be part of the API response. /// @@ -465,9 +465,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the app. /// The name of the schema. /// The ID of the field to disable. - /// Schema field deleted.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field deleted. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. [HttpDelete] [Route("apps/{app}/schemas/{schema}/fields/{id:long}/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -489,9 +489,9 @@ public sealed class SchemaFieldsController : ApiController /// The name of the schema. /// The parent field id. /// The ID of the field to disable. - /// Schema field deleted.. - /// Schema field request not valid or field locked.. - /// Schema, field or app not found.. + /// Schema field deleted. + /// Schema field request not valid or field locked. + /// Schema, field or app not found. [HttpDelete] [Route("apps/{app}/schemas/{schema}/fields/{parentId:long}/nested/{id:long}/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index e6a3f8a81..bab9a523d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -40,8 +40,8 @@ public sealed class SchemasController : ApiController /// Get schemas. /// /// The name of the app. - /// Schemas returned.. - /// App not found.. + /// Schemas returned. + /// App not found. [HttpGet] [Route("apps/{app}/schemas/")] [ProducesResponseType(typeof(SchemasDto), StatusCodes.Status200OK)] @@ -66,8 +66,8 @@ public sealed class SchemasController : ApiController /// /// The name of the app. /// The name of the schema to retrieve. - /// Schema found.. - /// Schema or app not found.. + /// Schema found. + /// Schema or app not found. [HttpGet] [Route("apps/{app}/schemas/{schema}/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -90,9 +90,9 @@ public sealed class SchemasController : ApiController /// /// The name of the app. /// The schema object that needs to be added to the app. - /// Schema created.. - /// Schema request not valid.. - /// Schema name already in use.. + /// Schema created. + /// Schema request not valid. + /// Schema name already in use. [HttpPost] [Route("apps/{app}/schemas/")] [ProducesResponseType(typeof(SchemaDto), 201)] @@ -113,9 +113,9 @@ public sealed class SchemasController : ApiController /// The name of the app. /// The name of the schema. /// The schema object that needs to updated. - /// Schema updated.. - /// Schema request not valid.. - /// Schema or app not found.. + /// Schema updated. + /// Schema request not valid. + /// Schema or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -136,9 +136,9 @@ public sealed class SchemasController : ApiController /// The name of the app. /// The name of the schema. /// The schema object that needs to updated. - /// Schema updated.. - /// Schema request not valid.. - /// Schema or app not found.. + /// Schema updated. + /// Schema request not valid. + /// Schema or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/sync")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -159,9 +159,9 @@ public sealed class SchemasController : ApiController /// The name of the app. /// The name of the schema. /// The schema object that needs to updated. - /// Schema updated.. - /// Schema request not valid.. - /// Schema or app not found.. + /// Schema updated. + /// Schema request not valid. + /// Schema or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/category")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -182,9 +182,9 @@ public sealed class SchemasController : ApiController /// The name of the app. /// The name of the schema. /// The preview urls for the schema. - /// Schema updated.. - /// Schema request not valid.. - /// Schema or app not found.. + /// Schema updated. + /// Schema request not valid. + /// Schema or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/preview-urls")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -205,9 +205,9 @@ public sealed class SchemasController : ApiController /// The name of the app. /// The name of the schema. /// The schema scripts object that needs to updated. - /// Schema updated.. - /// Schema request not valid.. - /// Schema or app not found.. + /// Schema updated. + /// Schema request not valid. + /// Schema or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/scripts/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -228,9 +228,9 @@ public sealed class SchemasController : ApiController /// The name of the app. /// The name of the schema. /// The schema rules object that needs to updated. - /// Schema updated.. - /// Schema request not valid.. - /// Schema or app not found.. + /// Schema updated. + /// Schema request not valid. + /// Schema or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/rules/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -250,8 +250,8 @@ public sealed class SchemasController : ApiController /// /// The name of the app. /// The name of the schema to publish. - /// Schema published.. - /// Schema or app not found.. + /// Schema published. + /// Schema or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/publish/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -271,8 +271,8 @@ public sealed class SchemasController : ApiController /// /// The name of the app. /// The name of the schema to unpublish. - /// Schema unpublished.. - /// Schema or app not found.. + /// Schema unpublished. + /// Schema or app not found. [HttpPut] [Route("apps/{app}/schemas/{schema}/unpublish/")] [ProducesResponseType(typeof(SchemaDto), StatusCodes.Status200OK)] @@ -292,8 +292,8 @@ public sealed class SchemasController : ApiController /// /// The name of the app. /// The name of the schema to delete. - /// Schema deleted.. - /// Schema or app not found.. + /// Schema deleted. + /// Schema or app not found. [HttpDelete] [Route("apps/{app}/schemas/{schema}/")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs b/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs index 8271cdd0a..ba4bfe8f4 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs @@ -33,8 +33,8 @@ public class SearchController : ApiController /// /// The name of the app. /// The search query. - /// Search results returned.. - /// App not found.. + /// Search results returned. + /// App not found. [HttpGet] [Route("apps/{app}/search/")] [ProducesResponseType(typeof(SearchResultDto[]), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 4c028e887..a770b8809 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -56,8 +56,8 @@ public sealed class UsagesController : ApiController /// Get api calls as log file. /// /// The name of the app. - /// Usage tracking results returned.. - /// App not found.. + /// Usage tracking results returned. + /// App not found. [HttpGet] [Route("apps/{app}/usages/log/")] [ProducesResponseType(typeof(LogDownloadDto), StatusCodes.Status200OK)] @@ -103,8 +103,8 @@ public sealed class UsagesController : ApiController /// The name of the app. /// The from date. /// The to date. - /// API call returned.. - /// App not found.. + /// API call returned. + /// App not found. [HttpGet] [Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")] [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] @@ -128,8 +128,8 @@ public sealed class UsagesController : ApiController /// The name of the team. /// The from date. /// The to date. - /// API call returned.. - /// Team not found.. + /// API call returned. + /// Team not found. [HttpGet] [Route("teams/{team}/usages/calls/{fromDate}/{toDate}/")] [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] @@ -151,8 +151,8 @@ public sealed class UsagesController : ApiController /// Get total asset size. /// /// The name of the app. - /// Storage usage returned.. - /// App not found.. + /// Storage usage returned. + /// App not found. [HttpGet] [Route("apps/{app}/usages/storage/today/")] [ProducesResponseType(typeof(CurrentStorageDto), StatusCodes.Status200OK)] @@ -174,8 +174,8 @@ public sealed class UsagesController : ApiController /// Get total asset size by team. /// /// The ID of the team. - /// Storage usage returned.. - /// Team not found.. + /// Storage usage returned. + /// Team not found. [HttpGet] [Route("teams/{team}/usages/storage/today/")] [ProducesResponseType(typeof(CurrentStorageDto), StatusCodes.Status200OK)] @@ -199,8 +199,8 @@ public sealed class UsagesController : ApiController /// The name of the app. /// The from date. /// The to date. - /// Storage usage returned.. - /// App not found.. + /// Storage usage returned. + /// App not found. [HttpGet] [Route("apps/{app}/usages/storage/{fromDate}/{toDate}/")] [ProducesResponseType(typeof(StorageUsagePerDateDto[]), StatusCodes.Status200OK)] @@ -221,8 +221,8 @@ public sealed class UsagesController : ApiController /// The ID of the team. /// The from date. /// The to date. - /// Storage usage returned.. - /// Team not found.. + /// Storage usage returned. + /// Team not found. [HttpGet] [Route("teams/{team}/usages/storage/{fromDate}/{toDate}/")] [ProducesResponseType(typeof(StorageUsagePerDateDto[]), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs index a8ae9959b..7594bd0b6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamContributorsController.cs @@ -36,8 +36,8 @@ public sealed class TeamContributorsController : ApiController /// Get team contributors. /// /// The ID of the team. - /// Contributors returned.. - /// Team not found.. + /// Contributors returned. + /// Team not found. [HttpGet] [Route("teams/{team}/contributors/")] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] @@ -60,9 +60,9 @@ public sealed class TeamContributorsController : ApiController /// /// The ID of the team. /// Contributor object that needs to be added to the team. - /// Contributor assigned to team.. - /// Contributor request not valid.. - /// Team not found.. + /// Contributor assigned to team. + /// Contributor request not valid. + /// Team not found. [HttpPost] [Route("teams/{team}/contributors/")] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status201Created)] @@ -81,8 +81,8 @@ public sealed class TeamContributorsController : ApiController /// Remove yourself. /// /// The ID of the team. - /// Contributor removed.. - /// Contributor or team not found.. + /// Contributor removed. + /// Contributor or team not found. [HttpDelete] [Route("teams/{team}/contributors/me/")] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] @@ -102,8 +102,8 @@ public sealed class TeamContributorsController : ApiController /// /// The ID of the team. /// The ID of the contributor. - /// Contributor removed.. - /// Contributor or team not found.. + /// Contributor removed. + /// Contributor or team not found. [HttpDelete] [Route("teams/{team}/contributors/{id}/")] [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs index e414697f9..e73f666ab 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Teams/TeamsController.cs @@ -33,7 +33,7 @@ public sealed class TeamsController : ApiController /// /// Get your teams. /// - /// Teams returned.. + /// Teams returned. /// /// You can only retrieve the list of teams when you are authenticated as a user (OpenID implicit flow). /// You will retrieve all teams, where you are assigned as a contributor. @@ -61,8 +61,8 @@ public sealed class TeamsController : ApiController /// Get an team by name. /// /// The name of the team. - /// Teams returned.. - /// Team not found.. + /// Teams returned. + /// Team not found. [HttpGet] [Route("teams/{team}")] [ProducesResponseType(typeof(TeamDto), StatusCodes.Status200OK)] @@ -84,9 +84,9 @@ public sealed class TeamsController : ApiController /// Create a new team. /// /// The team object that needs to be added to Squidex. - /// Team created.. - /// Team request not valid.. - /// Team name is already in use.. + /// Team created. + /// Team request not valid. + /// Team name is already in use. /// /// You can only create an team when you are authenticated as a user (OpenID implicit flow). /// You will be assigned as owner of the new team automatically. @@ -108,9 +108,9 @@ public sealed class TeamsController : ApiController /// /// The name of the team to update. /// The values to update. - /// Team updated.. - /// Team request not valid.. - /// Team not found.. + /// Team updated. + /// Team request not valid. + /// Team not found. [HttpPut] [Route("teams/{team}/")] [ProducesResponseType(typeof(TeamDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Templates/TemplatesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Templates/TemplatesController.cs index 4a531882b..9c9bb7d69 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Templates/TemplatesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Templates/TemplatesController.cs @@ -30,7 +30,7 @@ public sealed class TemplatesController : ApiController /// /// Get all templates. /// - /// Templates returned.. + /// Templates returned. [HttpGet] [Route("templates/")] [ProducesResponseType(typeof(TemplatesDto), StatusCodes.Status200OK)] @@ -48,8 +48,8 @@ public sealed class TemplatesController : ApiController /// Get template details. /// /// The name of the template. - /// Template returned.. - /// Template not found.. + /// Template returned. + /// Template not found. [HttpGet] [Route("templates/{name}")] [ProducesResponseType(typeof(TemplateDetailsDto), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs index 3ff65235f..8899dd06c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -37,7 +37,7 @@ public sealed class TranslationsController : ApiController /// /// The name of the app. /// The translation request. - /// Text translated.. + /// Text translated. [HttpPost] [Route("apps/{app}/translations/")] [ProducesResponseType(typeof(TranslationDto), StatusCodes.Status200OK)] @@ -56,7 +56,7 @@ public sealed class TranslationsController : ApiController /// /// The name of the app. /// The question request. - /// Question asked.. + /// Question asked. [HttpPost] [Route("apps/{app}/ask/")] [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs index 0798f6715..058226fdc 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs @@ -53,6 +53,9 @@ public sealed record MyUIOptions [JsonPropertyName("markerProject")] public string MarkerProject { get; set; } + [JsonPropertyName("collaborationService")] + public string CollaborationService { get; set; } + public sealed class MapOptions { [JsonPropertyName("type")] diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index cd3eda137..d27a40599 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -36,7 +36,7 @@ public sealed class UIController : ApiController /// /// Get ui settings. /// - /// UI settings returned.. + /// UI settings returned. [HttpGet] [Route("ui/settings/")] [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] @@ -63,8 +63,8 @@ public sealed class UIController : ApiController /// Get ui settings. /// /// The name of the app. - /// UI settings returned.. - /// App not found.. + /// UI settings returned. + /// App not found. [HttpGet] [Route("apps/{app}/ui/settings/")] [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] @@ -80,8 +80,8 @@ public sealed class UIController : ApiController /// Get my ui settings. /// /// The name of the app. - /// UI settings returned.. - /// App not found.. + /// UI settings returned. + /// App not found. [HttpGet] [Route("apps/{app}/ui/settings/me")] [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] @@ -98,8 +98,8 @@ public sealed class UIController : ApiController /// /// The name of the setting. /// The request with the value to update. - /// UI setting set.. - /// App not found.. + /// UI setting set. + /// App not found. [HttpPut] [Route("ui/settings/{key}")] [ApiPermission] @@ -116,8 +116,8 @@ public sealed class UIController : ApiController /// The name of the app. /// The name of the setting. /// The request with the value to update. - /// UI setting set.. - /// App not found.. + /// UI setting set. + /// App not found. [HttpPut] [Route("apps/{app}/ui/settings/{key}")] [ApiPermission] @@ -134,8 +134,8 @@ public sealed class UIController : ApiController /// The name of the app. /// The name of the setting. /// The request with the value to update. - /// UI setting set.. - /// App not found.. + /// UI setting set. + /// App not found. [HttpPut] [Route("apps/{app}/ui/settings/me/{key}")] [ApiPermission] @@ -150,8 +150,8 @@ public sealed class UIController : ApiController /// Remove ui settings. /// /// The name of the setting. - /// UI setting removed.. - /// App not found.. + /// UI setting removed. + /// App not found. [HttpDelete] [Route("ui/settings/{key}")] [ApiPermission] @@ -167,8 +167,8 @@ public sealed class UIController : ApiController /// /// The name of the app. /// The name of the setting. - /// UI setting removed.. - /// App not found.. + /// UI setting removed. + /// App not found. [HttpDelete] [Route("apps/{app}/ui/settings/{key}")] [ApiPermission] @@ -184,8 +184,8 @@ public sealed class UIController : ApiController /// /// The name of the app. /// The name of the setting. - /// UI setting removed.. - /// App not found.. + /// UI setting removed. + /// App not found. [HttpDelete] [Route("apps/{app}/ui/settings/me/{key}")] [ApiPermission] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs index 364d240bc..cc3d3ea1f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -37,7 +37,7 @@ public sealed class UserManagementController : ApiController /// Optional query to search by email address or username. /// The number of users to skip. /// The number of users to return. - /// Users returned.. + /// Users returned. [HttpGet] [Route("user-management/")] [ProducesResponseType(typeof(UsersDto), StatusCodes.Status200OK)] @@ -55,8 +55,8 @@ public sealed class UserManagementController : ApiController /// Get a user by ID. /// /// The ID of the user. - /// User returned.. - /// User not found.. + /// User returned. + /// User not found. [HttpGet] [Route("user-management/{id}/")] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] @@ -79,8 +79,8 @@ public sealed class UserManagementController : ApiController /// Create a new user. /// /// The user object that needs to be added. - /// User created.. - /// User request not valid.. + /// User created. + /// User request not valid. [HttpPost] [Route("user-management/")] [ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)] @@ -99,9 +99,9 @@ public sealed class UserManagementController : ApiController /// /// The ID of the user. /// The user object that needs to be updated. - /// User created.. - /// User request not valid.. - /// User not found.. + /// User created. + /// User request not valid. + /// User not found. [HttpPut] [Route("user-management/{id}/")] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] @@ -119,9 +119,9 @@ public sealed class UserManagementController : ApiController /// Lock a user. /// /// The ID of the user to lock. - /// User locked.. - /// User is the current user.. - /// User not found.. + /// User locked. + /// User is the current user. + /// User not found. [HttpPut] [Route("user-management/{id}/lock/")] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] @@ -144,9 +144,9 @@ public sealed class UserManagementController : ApiController /// Unlock a user. /// /// The ID of the user to unlock. - /// User unlocked.. - /// User is the current user.. - /// User not found.. + /// User unlocked. + /// User is the current user. + /// User not found. [HttpPut] [Route("user-management/{id}/unlock/")] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] @@ -169,9 +169,9 @@ public sealed class UserManagementController : ApiController /// Delete a User. /// /// The ID of the user to delete. - /// User deleted.. - /// User is the current user.. - /// User not found.. + /// User deleted. + /// User is the current user. + /// User not found. [HttpDelete] [Route("user-management/{id}/")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index 1d5b27a08..4d201235a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -60,7 +60,7 @@ public sealed class UsersController : ApiController /// /// Get the user resources. /// - /// User resources returned.. + /// User resources returned. [HttpGet] [Route("")] [ProducesResponseType(typeof(ResourcesDto), StatusCodes.Status200OK)] @@ -76,7 +76,7 @@ public sealed class UsersController : ApiController /// Update the user profile. /// /// The values to update. - /// User updated.. + /// User updated. [HttpPost] [Route("user")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -95,7 +95,7 @@ public sealed class UsersController : ApiController /// /// Search the user by query that contains the email address or the part of the email address. /// - /// Users returned.. + /// Users returned. [HttpGet] [Route("users/")] [ProducesResponseType(typeof(UserDto[]), StatusCodes.Status200OK)] @@ -122,8 +122,8 @@ public sealed class UsersController : ApiController /// Get user by id. /// /// The ID of the user (GUID). - /// User found.. - /// User not found.. + /// User found. + /// User not found. [HttpGet] [Route("users/{id}/")] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] @@ -153,8 +153,8 @@ public sealed class UsersController : ApiController /// Get user picture by id. /// /// The ID of the user (GUID). - /// User found and image or fallback returned.. - /// User not found.. + /// User found and image or fallback returned. + /// User not found. [HttpGet] [Route("users/{id}/picture/")] [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] diff --git a/backend/src/Squidex/Config/Domain/NotificationsServices.cs b/backend/src/Squidex/Config/Domain/CollaborationServices.cs similarity index 79% rename from backend/src/Squidex/Config/Domain/NotificationsServices.cs rename to backend/src/Squidex/Config/Domain/CollaborationServices.cs index d94f733b4..f23f36764 100644 --- a/backend/src/Squidex/Config/Domain/NotificationsServices.cs +++ b/backend/src/Squidex/Config/Domain/CollaborationServices.cs @@ -6,16 +6,17 @@ // ========================================================================== using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Entities.Collaboration; using Squidex.Domain.Apps.Entities.Invitation; -using Squidex.Domain.Apps.Entities.Notifications; using Squidex.Infrastructure.Email; using Squidex.Infrastructure.EventSourcing; +using YDotNet.Server; namespace Squidex.Config.Domain; -public static class NotificationsServices +public static class CollaborationServices { - public static void AddSquidexNotifications(this IServiceCollection services, IConfiguration config) + public static void AddSquidexCollaborations(this IServiceCollection services, IConfiguration config) { var emailOptions = config.GetSection("email:smtp").Get() ?? new (); @@ -40,5 +41,8 @@ public static class NotificationsServices services.AddSingletonAs() .As(); + + services.AddSingletonAs() + .As().As(); } } diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index fadc941de..8b92ebf66 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -12,7 +12,6 @@ using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.DomainObject; -using Squidex.Domain.Apps.Entities.Comments.DomainObject; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.DomainObject; using Squidex.Domain.Apps.Entities.Invitation; @@ -106,9 +105,6 @@ public static class CommandsServices services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/CommentsServices.cs b/backend/src/Squidex/Config/Domain/CommentsServices.cs deleted file mode 100644 index ffe4d0532..000000000 --- a/backend/src/Squidex/Config/Domain/CommentsServices.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Comments; - -namespace Squidex.Config.Domain; - -public static class CommentsServices -{ - public static void AddSquidexComments(this IServiceCollection services) - { - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - } -} diff --git a/backend/src/Squidex/Config/Domain/FontendServices.cs b/backend/src/Squidex/Config/Domain/FontendServices.cs index b16ec298c..79d7de7a4 100644 --- a/backend/src/Squidex/Config/Domain/FontendServices.cs +++ b/backend/src/Squidex/Config/Domain/FontendServices.cs @@ -9,6 +9,7 @@ using System.Text.Json; using Microsoft.Extensions.Options; using Squidex.Areas.Api.Controllers.UI; using Squidex.Domain.Apps.Entities.History; +using Squidex.Hosting; using Squidex.Text.ChatBots; using Squidex.Text.Translations; using Squidex.Web; @@ -54,5 +55,20 @@ public static class FontendServices options.More["canUseChatBot"] = chatBot.IsConfigured; }); + + services.Configure((services, options) => + { + if (string.IsNullOrWhiteSpace(options.CollaborationService)) + { + var urlGenerator = services.GetRequiredService(); + + var collaborationUrl = + urlGenerator.BuildUrl() + .Replace("https://", "wss://", StringComparison.OrdinalIgnoreCase) + .Replace("http://", "ws://", StringComparison.OrdinalIgnoreCase); + + options.CollaborationService = collaborationUrl; + } + }); } } diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index 7d845d82d..dea4f4c71 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Subscriptions; using Squidex.Domain.Apps.Core.Templates; using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Comments; +using Squidex.Domain.Apps.Entities.Collaboration; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Queries; diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index a73d7f554..6009244c9 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -50,6 +50,7 @@ using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; +using YDotNet.Server.MongoDB; namespace Squidex.Config.Domain; @@ -68,6 +69,12 @@ public static class StoreServices services.AddMongoAssetKeyValueStore(); services.AddSingleton(typeof(ISnapshotStore<>), typeof(MongoSnapshotStore<>)); + services.AddYDotNet() + .AddMongoStorage(options => + { + options.DatabaseName = mongoDatabaseName; + }); + services.AddSingletonAs(c => GetMongoClient(mongoConfiguration)) .As(); diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index 00456b0de..245ec9f39 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -75,6 +75,9 @@ public static class WebServices services.AddLocalization(); + services.AddYDotNet() + .AddWebSockets(); + services.AddMvc(options => { // Never change this order here. diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index be86b8a1a..55b82d17d 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -51,7 +51,7 @@ - + @@ -75,6 +75,12 @@ + + + + + + diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index 0ed289b9c..dc4a1bc17 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -45,8 +45,8 @@ public sealed class Startup services.AddSquidexAssetInfrastructure(config); services.AddSquidexAssets(config); services.AddSquidexBackups(); + services.AddSquidexCollaborations(config); services.AddSquidexCommands(config); - services.AddSquidexComments(); services.AddSquidexContents(config); services.AddSquidexControllerServices(config); services.AddSquidexEventSourcing(config); @@ -59,7 +59,6 @@ public sealed class Startup services.AddSquidexLocalization(); services.AddSquidexMessaging(config); services.AddSquidexMigration(config); - services.AddSquidexNotifications(config); services.AddSquidexOpenApiSettings(); services.AddSquidexQueries(config); services.AddSquidexRules(config); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs index 6bc29b1e4..cfba6cdb4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/UsageNotifierWorkerTest.cs @@ -8,7 +8,7 @@ using NodaTime; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Notifications; +using Squidex.Domain.Apps.Entities.Collaboration; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.TestHelpers; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentCollaborationHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentCollaborationHandlerTests.cs new file mode 100644 index 000000000..ca7b7fa2e --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentCollaborationHandlerTests.cs @@ -0,0 +1,335 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using LoremNET; +using Microsoft.Extensions.Logging; +using NodaTime; +using Squidex.Domain.Apps.Core.Comments; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Comments; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Shared.Users; +using YDotNet.Document; +using YDotNet.Document.Cells; +using YDotNet.Extensions; +using YDotNet.Server; + +namespace Squidex.Domain.Apps.Entities.Collaboration; + +public class CommentCollaborationHandlerTests : GivenContext +{ + private readonly SimpleDocumentManager documentManager = new SimpleDocumentManager(); + private readonly IClock clock = A.Fake(); + private readonly IEventFormatter eventFormatter = A.Fake(); + private readonly IEventStore eventStore = A.Fake(); + private readonly IUserResolver userResolver = A.Fake(); + private readonly CommentCollaborationHandler sut; + + public CommentCollaborationHandlerTests() + { + var now = SystemClock.Instance.GetCurrentInstant(); + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(now); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._, default)) + .Returns(Task.FromResult(null)); + + var log = A.Fake>(); + + sut = new CommentCollaborationHandler(TestUtils.DefaultSerializer, eventStore, eventFormatter, userResolver, clock, log); + } + + [Fact] + public void Should_provider_user_document_name() + { + var name = sut.UserDocument("user42"); + + Assert.Equal("users/user42", name); + } + + [Fact] + public void Should_provider_app_document_name() + { + var name = sut.ResourceDocument(AppId, DomainId.Create("resource42")); + + Assert.Equal($"apps/{AppId}/resource42", name); + } + + [Fact] + public async Task Should_create_comment() + { + await sut.OnInitializedAsync(documentManager); + + var commentsId = DomainId.Create("resource42"); + + var document = new Doc(); + var docName = sut.ResourceDocument(AppId, commentsId); + + documentManager.Doc = document; + + Output? addedInput = null; + document.Array("stream").ObserveDeep(events => + { + addedInput = events.Single().ArrayEvent.Delta.Single().Values.Single(); + }); + + await sut.CommentAsync(AppId, commentsId, "My Comment", User, null, true, default); + + var commentJson = addedInput!.ToJson(document); + var commentItem = TestUtils.DefaultSerializer.Deserialize(commentJson); + + commentItem.Should().BeEquivalentTo( + new Comment(clock.GetCurrentInstant(), User, "My Comment", null, true)); + } + + [Fact] + public async Task Should_create_notification() + { + await sut.OnInitializedAsync(documentManager); + + var document = new Doc(); + var docName = sut.UserDocument(User.Identifier); + + documentManager.Doc = document; + + Output? addedInput = null; + document.Array("stream").ObserveDeep(events => + { + addedInput = events.Single().ArrayEvent.Delta.Single().Values.Single(); + }); + + await sut.NotifyAsync(User.Identifier, "My Notification", User, null, true, default); + + var commentJson = addedInput!.ToJson(document); + var commentItem = TestUtils.DefaultSerializer.Deserialize(commentJson); + + commentItem.Should().BeEquivalentTo( + new Comment(clock.GetCurrentInstant(), User, "My Notification", null, true)); + } + + [Fact] + public async Task Should_publish_event_for_comment() + { + var text = "My Comment"; + + var commentsId = DomainId.Create("resource42"); + var commentItem = new Comment(clock.GetCurrentInstant(), User, text); + + var storedEvent = await CreateCommentAsync(commentsId, commentItem); + + storedEvent?.Payload.Should().BeEquivalentTo( + new CommentCreated + { + Actor = User, + AppId = AppId, + 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() + { + var text = "Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io"; + + var commentsId = DomainId.Create("resource42"); + var commentItem = new Comment(clock.GetCurrentInstant(), User, text); + + var storedEvent = await CreateCommentAsync(commentsId, commentItem); + + storedEvent?.Payload.Should().BeEquivalentTo( + new CommentCreated + { + Actor = User, + AppId = AppId, + CommentId = default, + CommentsId = commentsId, + Text = commentItem.Text, + Mentions = null, + }, opts => opts.Excluding(x => x.CommentId)); + } + + [Fact] + public async Task Should_enrich_comment_with_mentioned_users() + { + SetupUser("id1", "mail1@squidex.io"); + SetupUser("id2", "mail2@squidex.io"); + + var text = "Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io"; + + var commentsId = DomainId.Create("resource42"); + var commentItem = new Comment(clock.GetCurrentInstant(), User, text); + + var storedEvent = await CreateCommentAsync(commentsId, commentItem); + + storedEvent?.Payload.Should().BeEquivalentTo( + new CommentCreated + { + Actor = User, + AppId = AppId, + CommentId = default, + CommentsId = commentsId, + Text = commentItem.Text, + Mentions = new string[] { "id1", "id2" } + }, opts => opts.Excluding(x => x.CommentId)); + } + + [Fact] + public async Task Should_enrich_comment_with_mentioned_users_and_long_text() + { + SetupUser("id1", "mail1@squidex.io"); + SetupUser("id2", "mail2@squidex.io"); + + var text = $"Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io {Lorem.Paragraph(200, 10)}"; + + var commentsId = DomainId.Create("resource42"); + var commentItem = new Comment(clock.GetCurrentInstant(), User, text); + + var storedEvent = await CreateCommentAsync(commentsId, commentItem); + + storedEvent?.Payload.Should().BeEquivalentTo( + new CommentCreated + { + Actor = User, + AppId = AppId, + CommentId = default, + CommentsId = commentsId, + Text = commentItem.Text, + Mentions = new string[] { "id1", "id2" } + }, opts => opts.Excluding(x => x.CommentId)); + } + + private async Task?> CreateCommentAsync(DomainId commentsId, Comment comment) + { + var document = new Doc(); + var docName = sut.ResourceDocument(AppId, commentsId); + + 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(AppId.Id, 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); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(email, default)) + .Returns(user); + } + + private sealed class SimpleDocumentManager : IDocumentManager + { + private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); + + public Doc Doc { get; set; } + + public async ValueTask UpdateDocAsync(DocumentContext context, Action action, + CancellationToken ct = default) + { + await lockObject.WaitAsync(ct); + try + { + action(Doc); + } + finally + { + lockObject.Release(); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public ValueTask CleanupAsync( + CancellationToken ct = default) + { + return default; + } + + public ValueTask PingAsync(DocumentContext context, ulong clock, string? state = null, + CancellationToken ct = default) + { + return default; + } + + public ValueTask DisconnectAsync(DocumentContext context, + CancellationToken ct = default) + { + return default; + } + + public ValueTask> GetAwarenessAsync(DocumentContext context, + CancellationToken ct = default) + { + return default; + } + + public ValueTask GetStateVectorAsync(DocumentContext context, + CancellationToken ct = default) + { + return default; + } + + public ValueTask GetUpdateAsync(DocumentContext context, byte[] stateVector, + CancellationToken ct = default) + { + return default; + } + + public ValueTask ApplyUpdateAsync(DocumentContext context, byte[] stateDiff, + CancellationToken ct = default) + { + return default; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentTriggerHandlerTests.cs similarity index 98% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentTriggerHandlerTests.cs index db3057d0b..a03492f63 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/CommentTriggerHandlerTests.cs @@ -20,7 +20,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Comments; +namespace Squidex.Domain.Apps.Entities.Collaboration; public class CommentTriggerHandlerTests { @@ -51,12 +51,6 @@ public class CommentTriggerHandlerTests Assert.True(sut.Handles(new CommentCreated())); } - [Fact] - public void Should_not_handle_comment_update_event() - { - Assert.False(sut.Handles(new CommentUpdated())); - } - [Fact] public void Should_not_handle_other_event() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/EmailUserNotificationsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/EmailUserNotificationsTests.cs similarity index 99% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/EmailUserNotificationsTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/EmailUserNotificationsTests.cs index 673693afe..8b71d077c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/EmailUserNotificationsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Collaboration/EmailUserNotificationsTests.cs @@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.Email; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Notifications; +namespace Squidex.Domain.Apps.Entities.Collaboration; public class EmailUserNotificationsTests : GivenContext { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs deleted file mode 100644 index f00bf2395..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Comments.DomainObject; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Comments; - -public sealed class CommentsLoaderTests : GivenContext -{ - private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); - private readonly CommentsLoader sut; - - public CommentsLoaderTests() - { - sut = new CommentsLoader(domainObjectFactory); - } - - [Fact] - public async Task Should_get_comments_from_domain_object() - { - var commentsId = DomainId.NewGuid(); - var comments = new CommentsResult(); - - var domainObject = A.Fake(); - - A.CallTo(() => domainObjectFactory.Create(commentsId)) - .Returns(domainObject); - - A.CallTo(() => domainObject.GetComments(11)) - .Returns(comments); - - var actual = await sut.GetCommentsAsync(commentsId, 11, CancellationToken); - - Assert.Same(comments, actual); - - A.CallTo(() => domainObject.LoadAsync(CancellationToken)) - .MustHaveHappened(); - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs deleted file mode 100644 index 56355d997..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs +++ /dev/null @@ -1,175 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using LoremNET; -using Squidex.Domain.Apps.Core.TestHelpers; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject; - -public class CommentsCommandMiddlewareTests : GivenContext -{ - private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); - private readonly IUserResolver userResolver = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly DomainId commentsId = DomainId.NewGuid(); - private readonly DomainId commentId = DomainId.NewGuid(); - private readonly CommentsCommandMiddleware sut; - - public CommentsCommandMiddlewareTests() - { - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._, default)) - .Returns(Task.FromResult(null)); - - sut = new CommentsCommandMiddleware(domainObjectFactory, userResolver); - } - - [Fact] - public async Task Should_invoke_domain_object_for_comments_command() - { - var command = CreateCommentsCommand(new CreateComment()); - var context = CrateCommandContext(command); - - var domainObject = A.Fake(); - - A.CallTo(() => domainObject.ExecuteAsync(command, CancellationToken)) - .Returns(CommandResult.Empty(commentsId, 0, 0)); - - A.CallTo(() => domainObjectFactory.Create(commentsId)) - .Returns(domainObject); - - var isNextCalled = false; - - await sut.HandleAsync(context, (c, ct) => - { - isNextCalled = true; - - return Task.CompletedTask; - }, CancellationToken); - - Assert.True(isNextCalled); - } - - [Fact] - public async Task Should_enrich_with_mentioned_user_ids_if_found() - { - SetupUser("id1", "mail1@squidex.io"); - SetupUser("id2", "mail2@squidex.io"); - - var command = CreateCommentsCommand(new CreateComment - { - Text = "Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io", - IsMention = false - }); - - var context = CrateCommandContext(command); - - await sut.HandleAsync(context, CancellationToken); - - Assert.Equal(command.Mentions, new[] { "id1", "id2" }); - } - - [Fact] - public async Task Should_enrich_with_mentioned_user_ids_if_found_and_long_text() - { - SetupUser("id1", "mail1@squidex.io"); - SetupUser("id2", "mail2@squidex.io"); - - var command = CreateCommentsCommand(new CreateComment - { - Text = $"Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io {Lorem.Paragraph(200, 10)}", - IsMention = false - }); - - var context = CrateCommandContext(command); - - await sut.HandleAsync(context, CancellationToken); - - Assert.Equal(command.Mentions, new[] { "id1", "id2" }); - } - - [Fact] - public async Task Should_not_invoke_commands_for_mentioned_users() - { - SetupUser("id1", "mail1@squidex.io"); - SetupUser("id2", "mail2@squidex.io"); - - var command = CreateCommentsCommand(new CreateComment - { - Text = "Hi @mail1@squidex.io and @mail2@squidex.io", - IsMention = false - }); - - var context = CrateCommandContext(command); - - await sut.HandleAsync(context, CancellationToken); - - A.CallTo(() => commandBus.PublishAsync(A._, A._)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_enrich_with_mentioned_user_ids_if_invalid_mentioned_tags_used() - { - var command = CreateCommentsCommand(new CreateComment - { - Text = "Hi invalid@squidex.io", - IsMention = false - }); - - var context = CrateCommandContext(command); - - await sut.HandleAsync(context, CancellationToken); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._, A._)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_enrich_with_mentioned_user_ids_for_notification() - { - var command = CreateCommentsCommand(new CreateComment - { - Text = "Hi @invalid@squidex.io", - IsMention = true - }); - - var context = CrateCommandContext(command); - - await sut.HandleAsync(context, CancellationToken); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._, A._)) - .MustNotHaveHappened(); - } - - private CommandContext CrateCommandContext(ICommand command) - { - return new CommandContext(command, commandBus); - } - - private void SetupUser(string id, string email) - { - var user = UserMocks.User(id, email); - - A.CallTo(() => userResolver.FindByIdOrEmailAsync(email, default)) - .Returns(user); - } - - private T CreateCommentsCommand(T command) where T : CommentCommand - { - command.AppId = AppId; - command.CommentsId = commentsId; - command.CommentId = commentId; - command.Actor = User; - - return command; - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs deleted file mode 100644 index a4d622c3d..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs +++ /dev/null @@ -1,175 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Core.Comments; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Comments; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject; - -public class CommentsStreamTests -{ - private readonly IEventFormatter eventFormatter = A.Fake(); - private readonly IEventStore eventStore = A.Fake(); - private readonly DomainId commentsId = DomainId.NewGuid(); - private readonly DomainId commentId = DomainId.NewGuid(); - private readonly RefToken actor = RefToken.User("me"); - private readonly CommentsStream sut; - - public IEnumerable> LastEvents { get; private set; } = Enumerable.Empty>(); - - public CommentsStreamTests() - { - A.CallTo(() => eventStore.AppendAsync(A._, A._, A._, A>._, default)) - .Invokes(x => LastEvents = sut!.GetUncommittedEvents().Select(x => x.To()).ToList()); - - sut = new CommentsStream(commentsId, eventFormatter, eventStore); - } - - [Fact] - public async Task Create_should_create_events() - { - var command = new CreateComment { Text = "text1", Url = new Uri("http://uri") }; - - var actual = await sut.ExecuteAsync(CreateCommentsCommand(command), default); - - actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 0, EtagVersion.Empty)); - - sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult - { - Version = 0 - }); - - sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult - { - CreatedComments = new List - { - new Comment(command.CommentId, GetTime(), command.Actor, "text1", command.Url) - }, - Version = 0 - }); - - LastEvents - .ShouldHaveSameEvents( - CreateCommentsEvent(new CommentCreated { Text = command.Text, Url = command.Url }) - ); - } - - [Fact] - public async Task Update_should_create_events() - { - await ExecuteCreateAsync(); - - var updateCommand = new UpdateComment { Text = "text2" }; - - var actual = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand), default); - - actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 1, 0)); - - sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult - { - CreatedComments = new List - { - new Comment(commentId, GetTime(), updateCommand.Actor, "text2") - }, - Version = 1 - }); - - sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult - { - UpdatedComments = new List - { - new Comment(commentId, GetTime(), updateCommand.Actor, "text2") - }, - Version = 1 - }); - - LastEvents - .ShouldHaveSameEvents( - CreateCommentsEvent(new CommentUpdated { Text = updateCommand.Text }) - ); - } - - [Fact] - public async Task Delete_should_create_events() - { - await ExecuteCreateAsync(); - await ExecuteUpdateAsync(); - - var deleteCommand = new DeleteComment(); - - var actual = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand), default); - - actual.ShouldBeEquivalent(CommandResult.Empty(commentsId, 2, 1)); - - sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult - { - Version = 2 - }); - - sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult - { - DeletedComments = new List - { - commentId - }, - Version = 2 - }); - - sut.GetComments(1).Should().BeEquivalentTo(new CommentsResult - { - DeletedComments = new List - { - commentId - }, - Version = 2 - }); - - LastEvents - .ShouldHaveSameEvents( - CreateCommentsEvent(new CommentDeleted()) - ); - } - - private Task ExecuteCreateAsync() - { - return sut.ExecuteAsync(CreateCommentsCommand(new CreateComment { Text = "text1" }), default); - } - - private Task ExecuteUpdateAsync() - { - return sut.ExecuteAsync(CreateCommentsCommand(new UpdateComment { Text = "text2" }), default); - } - - private T CreateCommentsEvent(T @event) where T : CommentsEvent - { - @event.Actor = actor; - @event.CommentsId = commentsId; - @event.CommentId = commentId; - - return @event; - } - - private T CreateCommentsCommand(T command) where T : CommentCommand - { - command.Actor = actor; - command.CommentsId = commentsId; - command.CommentId = commentId; - - return command; - } - - private Instant GetTime() - { - return LastEvents.ElementAt(0).Headers.Timestamp(); - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/Guards/GuardCommentsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/Guards/GuardCommentsTests.cs deleted file mode 100644 index 134071626..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/Guards/GuardCommentsTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.TestHelpers; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Comments; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject.Guards; - -public class GuardCommentsTests : IClassFixture -{ - private readonly string commentsId = DomainId.NewGuid().ToString(); - private readonly RefToken user1 = RefToken.User("1"); - private readonly RefToken user2 = RefToken.User("2"); - - [Fact] - public void CanCreate_should_throw_exception_if_text_not_defined() - { - var command = new CreateComment(); - - ValidationAssert.Throws(() => GuardComments.CanCreate(command), - new ValidationError("Text is required.", "Text")); - } - - [Fact] - public void CanCreate_should_not_throw_exception_if_text_defined() - { - var command = new CreateComment { Text = "text" }; - - GuardComments.CanCreate(command); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_text_not_defined() - { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user1 }; - - var events = new List> - { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() - }; - - ValidationAssert.Throws(() => GuardComments.CanUpdate(command, commentsId, events), - new ValidationError("Text is required.", "Text")); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_comment_from_another_user() - { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user2, Text = "text2" }; - - var events = new List> - { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() - }; - - Assert.Throws(() => GuardComments.CanUpdate(command, commentsId, events)); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_comment_not_found() - { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user1 }; - - var events = new List>(); - - Assert.Throws(() => GuardComments.CanUpdate(command, commentsId, events)); - } - - [Fact] - public void CanUpdate_should_throw_exception_if_comment_deleted_found() - { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user1 }; - - var events = new List> - { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To(), - Envelope.Create(new CommentDeleted { CommentId = commentId }).To() - }; - - Assert.Throws(() => GuardComments.CanUpdate(command, commentsId, events)); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_comment_is_own_notification() - { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user1, Text = "text2" }; - - var events = new List> - { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() - }; - - GuardComments.CanUpdate(command, user1.Identifier, events); - } - - [Fact] - public void CanUpdate_should_not_throw_exception_if_comment_from_same_user() - { - var commentId = DomainId.NewGuid(); - var command = new UpdateComment { CommentId = commentId, Actor = user1, Text = "text2" }; - - var events = new List> - { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() - }; - - GuardComments.CanUpdate(command, commentsId, events); - } - - [Fact] - public void CanDelete_should_throw_exception_if_comment_from_another_user() - { - var commentId = DomainId.NewGuid(); - var command = new DeleteComment { CommentId = commentId, Actor = user2 }; - - var events = new List> - { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() - }; - - Assert.Throws(() => GuardComments.CanDelete(command, commentsId, events)); - } - - [Fact] - public void CanDelete_should_throw_exception_if_comment_not_found() - { - var commentId = DomainId.NewGuid(); - var command = new DeleteComment { CommentId = commentId, Actor = user1 }; - - var events = new List>(); - - Assert.Throws(() => GuardComments.CanDelete(command, commentsId, events)); - } - - [Fact] - public void CanDelete_should_throw_exception_if_comment_deleted() - { - var commentId = DomainId.NewGuid(); - var command = new DeleteComment { CommentId = commentId, Actor = user1 }; - - var events = new List> - { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }), - Envelope.Create(new CommentDeleted { CommentId = commentId }) - }; - - Assert.Throws(() => GuardComments.CanDelete(command, commentsId, events)); - } - - [Fact] - public void CanDelete_should_not_throw_exception_if_comment_is_own_notification() - { - var commentId = DomainId.NewGuid(); - var command = new DeleteComment { CommentId = commentId, Actor = user1 }; - - var events = new List> - { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() - }; - - GuardComments.CanDelete(command, user1.Identifier, events); - } - - [Fact] - public void CanDelete_should_not_throw_exception_if_comment_from_same_user() - { - var commentId = DomainId.NewGuid(); - var command = new DeleteComment { CommentId = commentId, Actor = user1 }; - - var events = new List> - { - Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To() - }; - - GuardComments.CanDelete(command, commentsId, events); - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs deleted file mode 100644 index e96a39a9a..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure.TestHelpers; - -namespace Squidex.Domain.Apps.Entities.Comments; - -public class WatchingServiceTests : GivenContext -{ - private readonly TestState state1; - private readonly TestState state2; - private readonly IClock clock = A.Fake(); - private readonly string resource1 = "resource1"; - private readonly string resource2 = "resource2"; - private readonly WatchingService sut; - private Instant now = SystemClock.Instance.GetCurrentInstant(); - - public WatchingServiceTests() - { - A.CallTo(() => clock.GetCurrentInstant()) - .ReturnsLazily(() => now); - - state1 = new TestState($"{AppId.Id}_{resource1}"); - state2 = new TestState($"{AppId.Id}_{resource2}", state1.PersistenceFactory); - - sut = new WatchingService(state1.PersistenceFactory) - { - Clock = clock - }; - } - - [Fact] - public async Task Should_only_return_self_if_no_one_watching() - { - var watching = await sut.GetWatchingUsersAsync(AppId.Id, resource1, "user1", CancellationToken); - - Assert.Equal(new[] { "user1" }, watching); - } - - [Fact] - public async Task Should_return_users_watching_on_same_resource() - { - await sut.GetWatchingUsersAsync(AppId.Id, resource1, "user1", CancellationToken); - await sut.GetWatchingUsersAsync(AppId.Id, resource2, "user2", CancellationToken); - - var watching1 = await sut.GetWatchingUsersAsync(AppId.Id, resource1, "user3", CancellationToken); - var watching2 = await sut.GetWatchingUsersAsync(AppId.Id, resource2, "user4", CancellationToken); - - Assert.Equal(new[] { "user1", "user3" }, watching1); - Assert.Equal(new[] { "user2", "user4" }, watching2); - } - - [Fact] - public async Task Should_cleanup_old_users() - { - await sut.GetWatchingUsersAsync(AppId.Id, resource1, "user1", CancellationToken); - await sut.GetWatchingUsersAsync(AppId.Id, resource2, "user2", CancellationToken); - - now = now.Plus(Duration.FromMinutes(2)); - - await sut.GetWatchingUsersAsync(AppId.Id, resource1, "user3", CancellationToken); - await sut.GetWatchingUsersAsync(AppId.Id, resource2, "user4", CancellationToken); - - var watching1 = await sut.GetWatchingUsersAsync(AppId.Id, resource1, "user5", CancellationToken); - var watching2 = await sut.GetWatchingUsersAsync(AppId.Id, resource2, "user6", CancellationToken); - - Assert.Equal(new[] { "user3", "user5" }, watching1); - Assert.Equal(new[] { "user4", "user6" }, watching2); - - A.CallTo(() => state1.Persistence.WriteSnapshotAsync(A._, CancellationToken)) - .MustHaveHappened(); - - A.CallTo(() => state2.Persistence.WriteSnapshotAsync(A._, CancellationToken)) - .MustHaveHappened(); - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs index d27aba9c6..67ee6f659 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Invitation/InvitationEventConsumerTests.cs @@ -9,8 +9,7 @@ using Microsoft.Extensions.Logging; using NodaTime; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Notifications; +using Squidex.Domain.Apps.Entities.Collaboration; using Squidex.Domain.Apps.Entities.Teams; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Apps; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index 87313fab7..21f5e6939 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -34,6 +34,7 @@ + diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs index c7227f5fb..0a19c38d6 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs @@ -6,8 +6,6 @@ // ========================================================================== using System.Globalization; -using System.Text.RegularExpressions; -using EventStore.Client; namespace Squidex.Infrastructure.EventSourcing; @@ -325,25 +323,6 @@ public abstract class EventStoreTests where T : IEventStore ShouldBeEquivalentTo(readEvents2, expected); } - [Theory] - [InlineData(5, 30)] - [InlineData(5, 300)] - public async Task Should_query_reverse(int commits, int count) - { - var streamName = $"test-{Guid.NewGuid()}"; - - var eventsWritten = await AppendEventsAsync(streamName, count, commits); - var eventsStored = eventsWritten.Select((x, i) => new StoredEvent(streamName, "Position", i, x)).ToArray(); - - for (var take = 0; take < count; take += count / 10) - { - var eventsExpected = eventsStored.TakeLast(take).ToArray(); - var eventsQueried = await Sut.QueryStreamReverseAsync(streamName, take); - - ShouldBeEquivalentTo(eventsQueried, eventsExpected); - } - } - [Theory] [InlineData(5, 30)] [InlineData(5, 300)] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 15c56dfd3..d60b0a766 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -37,7 +37,7 @@ "date-fns": "2.30.0", "font-awesome": "4.7.0", "graphiql": "3.0.5", - "graphql": "^16.8.1", + "graphql": "16.8.1", "graphql-ws": "^5.14.0", "image-focus": "1.2.1", "keycharm": "0.4.0", @@ -59,7 +59,6 @@ "rxjs": "7.8.1", "simplemde": "1.11.2", "slugify": "1.6.6", - "tinymce": "5.10.2", "tslib": "2.6.1", "tui-calendar": "^1.15.3", "typemoq": "^2.1.0", @@ -67,6 +66,8 @@ "vis-data": "7.1.6", "vis-network": "9.1.6", "vis-util": "5.0.3", + "y-protocols": "^1.0.6", + "y-websocket": "^1.5.0", "zone.js": "0.13.1" }, "devDependencies": { @@ -95,7 +96,6 @@ "@types/react-dom": "18.2.7", "@types/simplemde": "1.11.8", "@types/tapable": "2.2.3", - "@types/tinymce": "4.6.5", "@types/ws": "8.5.5", "@typescript-eslint/eslint-plugin": "6.2.1", "@typescript-eslint/parser": "6.2.1", @@ -316,7 +316,7 @@ "vite": "4.3.9", "webpack": "5.86.0", "webpack-dev-middleware": "6.1.1", - "webpack-dev-server": "4.15.0", + "webpack-dev-server": "4.15.1", "webpack-merge": "5.9.0", "webpack-subresource-integrity": "5.1.0" }, @@ -13586,14 +13586,6 @@ "integrity": "sha512-9YHUdvuNDDRJYXZwHqSsO72Ok0vmqoJbNn73ttyITQp/VA60SarnZ+MPLD37rJAhVoKp+9BWOvJP5tHIRfZylQ==", "dev": true }, - "node_modules/@types/jquery": { - "version": "3.5.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sizzle": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -13775,11 +13767,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/sizzle": { - "version": "2.3.3", - "dev": true, - "license": "MIT" - }, "node_modules/@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -13805,14 +13792,6 @@ "@types/estree": "*" } }, - "node_modules/@types/tinymce": { - "version": "4.6.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/jquery": "*" - } - }, "node_modules/@types/unist": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz", @@ -14320,6 +14299,22 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/abstract-leveldown": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/accepts": { "version": "1.3.8", "dev": true, @@ -14899,7 +14894,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true + "devOptional": true }, "node_modules/asynckit": { "version": "0.4.0", @@ -15187,7 +15182,7 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -15494,7 +15489,7 @@ }, "node_modules/buffer": { "version": "5.7.1", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -17070,6 +17065,19 @@ "clone": "^1.0.2" } }, + "node_modules/deferred-leveldown": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.1", + "inherits": "^2.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "dev": true, @@ -17504,6 +17512,21 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/encoding-down": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", + "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", + "optional": true, + "dependencies": { + "abstract-leveldown": "^6.2.1", + "inherits": "^2.0.3", + "level-codec": "^9.0.0", + "level-errors": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/encoding/node_modules/iconv-lite": { "version": "0.6.3", "dev": true, @@ -17627,7 +17650,6 @@ }, "node_modules/errno": { "version": "0.1.8", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -21021,7 +21043,7 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -21103,6 +21125,12 @@ "node": ">=0.10.0" } }, + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", + "optional": true + }, "node_modules/immutable": { "version": "4.0.0", "dev": true, @@ -21169,7 +21197,7 @@ }, "node_modules/inherits": { "version": "2.0.4", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/ini": { @@ -21797,6 +21825,15 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "dev": true, @@ -22878,6 +22915,139 @@ "node": ">=0.10.0" } }, + "node_modules/level": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz", + "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==", + "optional": true, + "dependencies": { + "level-js": "^5.0.0", + "level-packager": "^5.1.0", + "leveldown": "^5.4.0" + }, + "engines": { + "node": ">=8.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/level" + } + }, + "node_modules/level-codec": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", + "optional": true, + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-concat-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", + "optional": true, + "dependencies": { + "errno": "~0.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-iterator-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", + "optional": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.4.0", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-js": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz", + "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.3", + "buffer": "^5.5.0", + "inherits": "^2.0.3", + "ltgt": "^2.1.2" + } + }, + "node_modules/level-packager": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", + "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", + "optional": true, + "dependencies": { + "encoding-down": "^6.3.0", + "levelup": "^4.3.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-supports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", + "optional": true, + "dependencies": { + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leveldown": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz", + "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.1", + "napi-macros": "~2.0.0", + "node-gyp-build": "~4.1.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/levelup": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", + "optional": true, + "dependencies": { + "deferred-leveldown": "~5.3.0", + "level-errors": "~2.0.0", + "level-iterator-stream": "~4.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -22900,6 +23070,25 @@ "node": ">= 0.8.0" } }, + "node_modules/lib0": { + "version": "0.2.86", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.86.tgz", + "integrity": "sha512-kxigQTM4Q7NwJkEgdqQvU21qiR37twcqqLmh+/SbiGbRLfPlLVbHyY9sWp7PwXh0Xus9ELDSjsUOwcrdt5yZ4w==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/license-webpack-plugin": { "version": "4.0.2", "dev": true, @@ -22969,8 +23158,7 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -23123,6 +23311,12 @@ "node": ">=10" } }, + "node_modules/ltgt": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==", + "optional": true + }, "node_modules/lunr": { "version": "2.3.9", "dev": true, @@ -24156,6 +24350,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-macros": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, @@ -24380,6 +24580,17 @@ "node": "^12.13 || ^14.13 || >=16" } }, + "node_modules/node-gyp-build": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", + "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-gyp/node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -26012,7 +26223,6 @@ }, "node_modules/prr": { "version": "1.0.1", - "dev": true, "license": "MIT", "optional": true }, @@ -26588,7 +26798,7 @@ }, "node_modules/readable-stream": { "version": "3.6.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -28192,7 +28402,7 @@ }, "node_modules/string_decoder": { "version": "1.3.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -28200,7 +28410,7 @@ }, "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.2.1", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -29111,11 +29321,6 @@ "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", "dev": true }, - "node_modules/tinymce": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.10.2.tgz", - "integrity": "sha512-5QhnZ6c8F28fYucLLc00MM37fZoAZ4g7QCYzwIl38i5TwJR5xGqzOv6YMideyLM4tytCzLCRwJoQen2LI66p5A==" - }, "node_modules/tmp": { "version": "0.0.33", "dev": true, @@ -29895,7 +30100,7 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/utila": { @@ -30229,9 +30434,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.0.tgz", - "integrity": "sha512-HmNB5QeSl1KpulTBQ8UT4FPrByYyaLxpJoQ0+s7EvUrMc16m0ZS1sgb1XGqzmgCPk0c9y+aaXxn11tbLzuM7NQ==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", "dev": true, "dependencies": { "@types/bonjour": "^3.5.9", @@ -30240,7 +30445,7 @@ "@types/serve-index": "^1.9.1", "@types/serve-static": "^1.13.10", "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", + "@types/ws": "^8.5.5", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.0.11", "chokidar": "^3.5.3", @@ -30736,11 +30941,81 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.4" } }, + "node_modules/y-leveldb": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.2.tgz", + "integrity": "sha512-6ulEn5AXfXJYi89rXPEg2mMHAyyw8+ZfeMMdOtBbV8FJpQ1NOrcgi6DTAcXof0dap84NjHPT2+9d0rb6cFsjEg==", + "optional": true, + "dependencies": { + "level": "^6.0.1", + "lib0": "^0.2.31" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-websocket": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-1.5.0.tgz", + "integrity": "sha512-A8AO6XtnQlYwWFytWdkDCeXg4l8ghRTIw5h2YUgUYDmEC9ugWGIwYNW80yadhSFAF7CvuWTEkQNEpevnH6EiZw==", + "dependencies": { + "lib0": "^0.2.52", + "lodash.debounce": "^4.0.8", + "y-protocols": "^1.0.5" + }, + "bin": { + "y-websocket": "bin/server.js", + "y-websocket-server": "bin/server.js" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "optionalDependencies": { + "ws": "^6.2.1", + "y-leveldb": "^0.1.0" + }, + "peerDependencies": { + "yjs": "^13.5.6" + } + }, + "node_modules/y-websocket/node_modules/ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "optional": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "license": "ISC", @@ -30809,6 +31084,23 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yjs": { + "version": "13.6.8", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.8.tgz", + "integrity": "sha512-ZPq0hpJQb6f59B++Ngg4cKexDJTvfOgeiv0sBc4sUm8CaBWH7OQC4kcCgrqbjJ/B2+6vO49exvTmYfdlPtcjbg==", + "peer": true, + "dependencies": { + "lib0": "^0.2.74" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yn": { "version": "3.1.1", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index 774550e31..ac3eef418 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -73,6 +73,8 @@ "vis-data": "7.1.6", "vis-network": "9.1.6", "vis-util": "5.0.3", + "y-protocols": "^1.0.6", + "y-websocket": "^1.5.0", "zone.js": "0.13.1" }, "devDependencies": { diff --git a/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index 62cc0dc0f..924abb45c 100644 --- a/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -9,27 +9,28 @@ import { Component, OnInit } from '@angular/core'; import { timer } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { EventConsumerDto, EventConsumersState } from '@app/features/administration/internal'; -import { DialogModel, ResourceOwner } from '@app/shared'; +import { DialogModel, Subscriptions } from '@app/shared'; @Component({ selector: 'sqx-event-consumers-page', styleUrls: ['./event-consumers-page.component.scss'], templateUrl: './event-consumers-page.component.html', }) -export class EventConsumersPageComponent extends ResourceOwner implements OnInit { +export class EventConsumersPageComponent implements OnInit { + private readonly subscriptions = new Subscriptions(); + public eventConsumerErrorDialog = new DialogModel(); public eventConsumerError?: string; constructor( public readonly eventConsumersState: EventConsumersState, ) { - super(); } public ngOnInit() { this.eventConsumersState.load(); - this.own(timer(1000, 1000).pipe(switchMap(() => this.eventConsumersState.load(false, true)))); + this.subscriptions.add(timer(1000, 1000).pipe(switchMap(() => this.eventConsumersState.load(false, true)))); } public reload() { diff --git a/frontend/src/app/features/administration/pages/users/user-page.component.ts b/frontend/src/app/features/administration/pages/users/user-page.component.ts index da8f2d2e7..59168ea12 100644 --- a/frontend/src/app/features/administration/pages/users/user-page.component.ts +++ b/frontend/src/app/features/administration/pages/users/user-page.component.ts @@ -8,14 +8,16 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { UpsertUserDto, UserDto, UserForm, UsersState } from '@app/features/administration/internal'; -import { ResourceOwner } from '@app/shared'; +import { Subscriptions } from '@app/shared'; @Component({ selector: 'sqx-user-page', styleUrls: ['./user-page.component.scss'], templateUrl: './user-page.component.html', }) -export class UserPageComponent extends ResourceOwner implements OnInit { +export class UserPageComponent implements OnInit { + private readonly subscriptions = new Subscriptions(); + public isEditable = false; public user?: UserDto | null; @@ -26,11 +28,10 @@ export class UserPageComponent extends ResourceOwner implements OnInit { private readonly route: ActivatedRoute, private readonly router: Router, ) { - super(); } public ngOnInit() { - this.own( + this.subscriptions.add( this.usersState.selectedUser .subscribe(user => { this.user = user; diff --git a/frontend/src/app/features/administration/pages/users/users-page.component.ts b/frontend/src/app/features/administration/pages/users/users-page.component.ts index 00741ff27..550609542 100644 --- a/frontend/src/app/features/administration/pages/users/users-page.component.ts +++ b/frontend/src/app/features/administration/pages/users/users-page.component.ts @@ -8,7 +8,7 @@ import { Component, OnInit } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; import { UserDto, UsersState } from '@app/features/administration/internal'; -import { ResourceOwner, Router2State } from '@app/framework'; +import { Router2State, Subscriptions } from '@app/framework'; @Component({ selector: 'sqx-users-page', @@ -18,16 +18,16 @@ import { ResourceOwner, Router2State } from '@app/framework'; Router2State, ], }) -export class UsersPageComponent extends ResourceOwner implements OnInit { +export class UsersPageComponent implements OnInit { + private readonly subscriptions = new Subscriptions(); + public usersFilter = new UntypedFormControl(); constructor( public readonly usersRoute: Router2State, public readonly usersState: UsersState, ) { - super(); - - this.own( + this.subscriptions.add( this.usersState.query .subscribe(q => this.usersFilter.setValue(q || ''))); } diff --git a/frontend/src/app/features/assets/pages/assets-page.component.ts b/frontend/src/app/features/assets/pages/assets-page.component.ts index fe7259c3b..588651a3a 100644 --- a/frontend/src/app/features/assets/pages/assets-page.component.ts +++ b/frontend/src/app/features/assets/pages/assets-page.component.ts @@ -6,7 +6,7 @@ */ import { Component, OnInit } from '@angular/core'; -import { AssetDto, AssetsState, DialogModel, LocalStoreService, MathHelper, Queries, Query, QueryFullTextSynchronizer, ResourceOwner, Router2State, UIState } from '@app/shared'; +import { AssetDto, AssetsState, DialogModel, LocalStoreService, MathHelper, Queries, Query, QueryFullTextSynchronizer, Router2State, UIState } from '@app/shared'; import { Settings } from '@app/shared/state/settings'; @Component({ @@ -17,7 +17,7 @@ import { Settings } from '@app/shared/state/settings'; Router2State, ], }) -export class AssetsPageComponent extends ResourceOwner implements OnInit { +export class AssetsPageComponent implements OnInit { public editAsset?: AssetDto; public listQueries = new Queries(this.uiState, 'assets'); @@ -31,8 +31,6 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit { private readonly localStore: LocalStoreService, private readonly uiState: UIState, ) { - super(); - this.listMode = this.localStore.getBoolean(Settings.Local.ASSETS_MODE); } diff --git a/frontend/src/app/features/content/pages/content/content-history-page.component.ts b/frontend/src/app/features/content/pages/content/content-history-page.component.ts index be92f9fdb..a345736ec 100644 --- a/frontend/src/app/features/content/pages/content/content-history-page.component.ts +++ b/frontend/src/app/features/content/pages/content/content-history-page.component.ts @@ -8,7 +8,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { Observable, timer } from 'rxjs'; import { map } from 'rxjs/operators'; -import { AppsState, ContentDto, ContentsState, defined, HistoryEventDto, HistoryService, ModalModel, ResourceOwner, SchemasState, switchSafe } from '@app/shared'; +import { AppsState, ContentDto, ContentsState, defined, HistoryEventDto, HistoryService, ModalModel, SchemasState, Subscriptions, switchSafe } from '@app/shared'; import { DueTimeSelectorComponent } from './../../shared/due-time-selector.component'; import { ContentPageComponent } from './content-page.component'; @@ -17,7 +17,9 @@ import { ContentPageComponent } from './content-page.component'; styleUrls: ['./content-history-page.component.scss'], templateUrl: './content-history-page.component.html', }) -export class ContentHistoryPageComponent extends ResourceOwner implements OnInit { +export class ContentHistoryPageComponent implements OnInit { + private readonly subscriptions = new Subscriptions(); + @ViewChild('dueTimeSelector', { static: false }) public dueTimeSelector!: DueTimeSelectorComponent; @@ -38,11 +40,10 @@ export class ContentHistoryPageComponent extends ResourceOwner implements OnInit private readonly historyService: HistoryService, private readonly schemasState: SchemasState, ) { - super(); } public ngOnInit() { - this.own( + this.subscriptions.add( this.contentsState.selectedContent.pipe(defined()) .subscribe(content => { this.content = content; diff --git a/frontend/src/app/features/content/pages/content/content-page.component.html b/frontend/src/app/features/content/pages/content/content-page.component.html index 374524299..3cad2e815 100644 --- a/frontend/src/app/features/content/pages/content/content-page.component.html +++ b/frontend/src/app/features/content/pages/content/content-page.component.html @@ -51,7 +51,7 @@