diff --git a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaFlowStep.cs index 74fb954b6..227d615d8 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaFlowStep.cs @@ -68,7 +68,7 @@ public record AlgoliaFlowStep : FlowStep, IConvertibleToAction { var @event = ((FlowEventContext)executionContext.Context).Event; - if (!@event.ShouldDelete(executionContext, Delete)) + if (@event.ShouldDelete(executionContext, Delete)) { Document = null; return default; diff --git a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchFlowStep.cs index e0eae34bc..4d4e97121 100644 --- a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchFlowStep.cs @@ -74,7 +74,7 @@ public sealed record ElasticSearchFlowStep : FlowStep, IConvertibleToAction { var @event = ((FlowEventContext)executionContext.Context).Event; - if (!@event.ShouldDelete(executionContext, Delete)) + if (@event.ShouldDelete(executionContext, Delete)) { Document = null; return default; diff --git a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchFlowStep.cs index a757be9d4..16ff98fd9 100644 --- a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchFlowStep.cs @@ -76,30 +76,28 @@ public sealed record OpenSearchFlowStep : FlowStep, IConvertibleToAction if (@event.ShouldDelete(executionContext, Delete)) { - OpenSearchContent content; - try - { - content = executionContext.DeserializeJson(Document!); - } - catch (Exception ex) - { - content = new OpenSearchContent - { - More = new Dictionary - { - ["error"] = $"Invalid JSON: {ex.Message}", - }, - }; - } + Document = null; + return default; + } - Document = executionContext.SerializeJson(content); + OpenSearchContent content; + try + { + content = executionContext.DeserializeJson(Document!); } - else + catch (Exception ex) { - Document = null; + content = new OpenSearchContent + { + More = new Dictionary + { + ["error"] = $"Invalid JSON: {ex.Message}", + }, + }; } - return base.PrepareAsync(executionContext, ct); + Document = executionContext.SerializeJson(content); + return default; } public override async ValueTask ExecuteAsync(FlowExecutionContext executionContext, diff --git a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseFlowStep.cs index e97c0502a..6c850fdaa 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseFlowStep.cs @@ -59,7 +59,7 @@ public sealed record TypesenseFlowStep : FlowStep, IConvertibleToAction { var @event = ((FlowEventContext)executionContext.Context).Event; - if (!@event.ShouldDelete(executionContext, Delete)) + if (@event.ShouldDelete(executionContext, Delete)) { Document = null; return default; @@ -84,8 +84,7 @@ public sealed record TypesenseFlowStep : FlowStep, IConvertibleToAction } Document = executionContext.SerializeJson(content); - - return base.PrepareAsync(executionContext, ct); + return default; } public override async ValueTask ExecuteAsync(FlowExecutionContext executionContext, diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/CollectionProvider.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/CollectionProvider.cs index 46e965f52..673a23750 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/CollectionProvider.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/CollectionProvider.cs @@ -41,7 +41,7 @@ internal class CollectionProvider(IMongoClient mongoClient, string prefixDatabas .Ascending(x => x.IndexedSchemaId) .Ascending(x => x.IsDeleted) .Descending(x => x.LastModified)), - ]); + ]); return schemaCollection; } diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/AtlasTextIndex.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/AtlasTextIndex.cs index bbf9b22b9..a3345db63 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/AtlasTextIndex.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/AtlasTextIndex.cs @@ -17,7 +17,8 @@ using LuceneQueryAnalyzer = Lucene.Net.QueryParsers.Classic.QueryParser; namespace Squidex.Domain.Apps.Entities.Contents.Text; -public sealed class AtlasTextIndex(IMongoDatabase database, IHttpClientFactory atlasClient, IOptions atlasOptions, string shardKey) : MongoTextIndexBase>(database, shardKey, new CommandFactory>(BuildTexts)) +public sealed class AtlasTextIndex(IMongoDatabase database, IHttpClientFactory atlasClient, IOptions atlasOptions, string shardKey) + : MongoTextIndexBase>(database, shardKey, new CommandFactory>(BuildTexts)) { private static readonly LuceneQueryVisitor QueryVisitor = new LuceneQueryVisitor(AtlasIndexDefinition.GetFieldPath); private static readonly LuceneQueryAnalyzer QueryParser = diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/DocumentDbTextIndex.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/DocumentDbTextIndex.cs new file mode 100644 index 000000000..ecc96f476 --- /dev/null +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/DocumentDbTextIndex.cs @@ -0,0 +1,179 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Driver; +using MongoDB.Driver.GeoJsonObjectModel; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.ObjectPool; +using Squidex.Infrastructure.Translations; + +namespace Squidex.Domain.Apps.Entities.Contents.Text; + +public sealed class DocumentDbTextIndex(IMongoDatabase database, string shardKey) + : MongoTextIndexBase(database, shardKey, new CommandFactory(BuildTexts)) +{ + private record struct SearchOperation + { + required public App App { get; init; } + + required public List<(DomainId Id, double Score)> Results { get; init; } + + required public string SearchTerms { get; init; } + + required public int Take { get; set; } + + required public SearchScope SearchScope { get; init; } + } + + protected override async Task SetupCollectionAsync(IMongoCollection> collection, + CancellationToken ct) + { + await collection.Indexes.CreateOneAsync( + new CreateIndexModel>( + Index.Text(x => x.Texts)), + cancellationToken: ct); + + + await collection.Indexes.CreateManyAsync( + [ + new CreateIndexModel>( + Index + .Ascending(x => x.AppId) + .Ascending(x => x.ContentId)), + + new CreateIndexModel>( + Index + .Ascending(x => x.AppId) + .Ascending(x => x.SchemaId) + .Ascending(x => x.GeoField) + .Geo2DSphere(x => x.GeoObject)), + ], ct); + } + + public override async Task?> SearchAsync(App app, GeoQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); + + var point = new GeoJsonPoint(new GeoJson2DCoordinates(query.Longitude, query.Latitude)); + + // Use the filter in the correct order to leverage the index in the best way. + var findFilter = + Filter.And( + Filter.Eq(x => x.AppId, app.Id), + Filter.Eq(x => x.SchemaId, query.SchemaId), + Filter.Eq(x => x.GeoField, query.Field), + Filter.NearSphere(x => x.GeoObject, point, query.Radius), + FilterByScope(scope)); + + var byGeo = + await GetCollection(scope).Find(findFilter).Limit(query.Take) + .Project(Projection.Include(x => x.ContentId)) + .ToListAsync(ct); + + return byGeo.Select(x => x.ContentId).ToList(); + } + + public override async Task?> SearchAsync(App app, TextQuery query, SearchScope scope, + CancellationToken ct = default) + { + Guard.NotNull(app); + Guard.NotNull(query); + + if (string.IsNullOrWhiteSpace(query.Text)) + { + return null; + } + + // Use a custom tokenizer to leverage stop words from multiple languages. + var search = new SearchOperation + { + App = app, + SearchTerms = Tokenizer.Query(query.Text), + SearchScope = scope, + Results = [], + Take = query.Take, + }; + + if (query.RequiredSchemaIds?.Count > 0) + { + await SearchBySchemaAsync(search, query.RequiredSchemaIds, 1, ct); + } + else if (query.PreferredSchemaId == null) + { + await SearchByAppAsync(search, 1, ct); + } + else + { + // We cannot write queries that prefer results from the same schema, therefore make two queries. + search.Take /= 2; + + // Increasing the scoring of the results from the schema by 10 percent. + await SearchBySchemaAsync(search, Enumerable.Repeat(query.PreferredSchemaId.Value, 1), 1.1, ct); + await SearchByAppAsync(search, 1, ct); + } + + return search.Results.OrderByDescending(x => x.Score).Select(x => x.Id).Distinct().ToList(); + } + + private Task SearchBySchemaAsync(SearchOperation search, IEnumerable schemaIds, double factor, + CancellationToken ct) + { + var filter = + Filter.And( + Filter.Eq(x => x.AppId, search.App.Id), + Filter.Text(search.SearchTerms), + Filter.In(x => x.SchemaId, schemaIds), + FilterByScope(search.SearchScope)); + + return SearchAsync(search, filter, factor, ct); + } + + private Task SearchByAppAsync(SearchOperation search, double factor, + CancellationToken ct) + { + var filter = + Filter.And( + Filter.Eq(x => x.AppId, search.App.Id), + Filter.Text(search.SearchTerms), + FilterByScope(search.SearchScope)); + + return SearchAsync(search, filter, factor, ct); + } + + private async Task SearchAsync(SearchOperation search, FilterDefinition> filter, double factor, + CancellationToken ct) + { + var byText = + await GetCollection(search.SearchScope).Find(filter).Limit(search.Take) + .Project(Projection.Include(x => x.ContentId).MetaTextScore("score")).Sort(Sort.MetaTextScore("score")) + .ToListAsync(ct); + + search.Results.AddRange(byText.Select(x => (x.ContentId, x.Score * factor))); + } + + private static string BuildTexts(Dictionary source) + { + var sb = DefaultPools.StringBuilder.Get(); + try + { + foreach (var (key, value) in source) + { + sb.Append(' '); + sb.Append(Tokenizer.Terms(value, key)); + } + + return sb.ToString(); + } + finally + { + DefaultPools.StringBuilder.Return(sb); + } + } +} diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndex.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndex.cs index 43bbec04d..d64ec1dff 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndex.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndex.cs @@ -11,7 +11,8 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.Text; -public sealed class MongoTextIndex(IMongoDatabase database, string shardKey) : MongoTextIndexBase>(database, shardKey, new CommandFactory>(BuildTexts)) +public sealed class MongoTextIndex(IMongoDatabase database, string shardKey) + : MongoTextIndexBase>(database, shardKey, new CommandFactory>(BuildTexts)) { private record struct SearchOperation { @@ -33,9 +34,7 @@ public sealed class MongoTextIndex(IMongoDatabase database, string shardKey) : M await collection.Indexes.CreateOneAsync( new CreateIndexModel>>( - Index - .Ascending(x => x.AppId) - .Text("t.t")), + Index.Ascending(x => x.AppId).Text("t.t")), cancellationToken: ct); } diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndexBase.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndexBase.cs index a7efe54d5..f4ab89dfd 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndexBase.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndexBase.cs @@ -114,7 +114,9 @@ public abstract class MongoTextIndexBase(IMongoDatabase database, string shar catch (MongoBulkWriteException ex) { // Ignore invalid geo data when writing content. Our insert is unordered anyway. - if (ex.WriteErrors.Any(error => error.Code != MongoDbErrorCodes.Errror16755_InvalidGeoData)) + if (!ex.WriteErrors.All(e => + MongoDbErrorCodes.IsInvalidGeoData(e) || + MongoDbErrorCodes.IsInvalidDocumentDbGeoData(e))) { throw; } diff --git a/backend/src/Squidex.Data.MongoDb/Infrastructure/MongoDbErrorCodes.cs b/backend/src/Squidex.Data.MongoDb/Infrastructure/MongoDbErrorCodes.cs index 8fb8cb4dd..ecd1c9b81 100644 --- a/backend/src/Squidex.Data.MongoDb/Infrastructure/MongoDbErrorCodes.cs +++ b/backend/src/Squidex.Data.MongoDb/Infrastructure/MongoDbErrorCodes.cs @@ -7,9 +7,19 @@ #pragma warning disable SA1310 // Field names should not contain underscore +using MongoDB.Driver; + namespace Squidex.Infrastructure; public static class MongoDbErrorCodes { - public const int Errror16755_InvalidGeoData = 16755; + public static bool IsInvalidGeoData(WriteError error) + { + return error.Code == 16755; + } + + public static bool IsInvalidDocumentDbGeoData(WriteError error) + { + return error.Code == 2; + } } diff --git a/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs b/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs index 544a43e0a..a3b226284 100644 --- a/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs +++ b/backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs @@ -228,6 +228,14 @@ public static class ServiceExtensions shardKey => ActivatorUtilities.CreateInstance(c, shardKey)); }).AsOptional().As(); } + else if (config.GetValue("store:mongoDb:dpcumentDb")) + { + services.AddSingletonAs(c => + { + return new MongoShardedTextIndex(GetSharding(config, "store:mongoDB:textShardCount"), + shardKey => ActivatorUtilities.CreateInstance(c, shardKey)); + }).AsOptional().As(); + } else { services.AddSingletonAs(c => diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleQueueWriter.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleQueueWriter.cs index e27345ca8..a7b0b8e79 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleQueueWriter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleQueueWriter.cs @@ -26,7 +26,9 @@ public sealed class RuleQueueWriter(IFlowManager flowManager, Guard.NotNull(result); // We do not want to handle events without a job in the normal flow. - if (result.Job == null || result.Rule == null) + if (result.Job == null || + result.Rule == null || + result.SkipReason is not(SkipReason.None or SkipReason.Disabled)) { return false; } diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 65662225d..ba8150c63 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -503,10 +503,10 @@ }, "amazonS3": { // The url of the S3 API service. Leave it empty if using the one provided by Amazon - "serviceUrl": "", + "serviceUrl": "https://ams1.vultrobjects.com", // The name of your bucket. - "bucket": "squidex-test", + "bucket": "squidex", // The optional folder within the bucket. "bucketFolder": "squidex-assets", @@ -517,12 +517,12 @@ // The access key for your user. // // Read More: https://supsystic.com/documentation/id-secret-access-key-amazon-s3/ - "accessKey": "", + "accessKey": "4U9IPATBRAZ2D0LOFS4P", // The secret key for your user. // // Read More: https://supsystic.com/documentation/id-secret-access-key-amazon-s3/ - "secretKey": "", + "secretKey": "AwNBNWnlmBvyrI9jGD77hcD9Bqo1HcWtlnEKNbhn", // True, to disable the SigV4 payload signing. // @@ -647,6 +647,9 @@ // SUPPORTED: Document, String, Binary (from slow to fast). "valueRepresentation": "Undefined", + // Indicates that we use document db. + "documentDb": false, + "atlas": { // The organization id. "groupId": "", diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/DocumentDbTextIndexTests.cs b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/DocumentDbTextIndexTests.cs new file mode 100644 index 000000000..17cdb1df6 --- /dev/null +++ b/backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/DocumentDbTextIndexTests.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Contents.Text; +using Squidex.MongoDb.TestHelpers; + +namespace Squidex.MongoDb.Domain.Contents.Text; + +[Trait("Category", "Dependencies")] +[Collection(DocumentDbCollection.Name)] +public sealed class DocumentDbTextIndexTests(DocumentDbFixture fixture) : TextIndexerTests +{ + public override bool SupportsQuerySyntax => false; + + public override bool SupportsGeo => true; + + public override async Task CreateSutAsync() + { + var sut = new DocumentDbTextIndex(fixture.Database, string.Empty); + + await sut.InitializeAsync(default); + return sut; + } + + [Fact] + public async Task Should_retrieve_all_stopwords_for_german_query() + { + await CreateTextAsync(Ids1[0], "de", "and und"); + await CreateTextAsync(Ids2[0], "en", "and und"); + + await SearchText(expected: Ids2, text: "en:und"); + } +} diff --git a/backend/tests/Squidex.Data.Tests/MongoDb/TestHelpers/DocumentDbFixture.cs b/backend/tests/Squidex.Data.Tests/MongoDb/TestHelpers/DocumentDbFixture.cs new file mode 100644 index 000000000..f82b69e5d --- /dev/null +++ b/backend/tests/Squidex.Data.Tests/MongoDb/TestHelpers/DocumentDbFixture.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.TestHelpers; + +#pragma warning disable MA0048 // File name must match type name + +namespace Squidex.MongoDb.TestHelpers; + +[CollectionDefinition(Name)] +public sealed class DocumentDbCollection : ICollectionFixture +{ + public const string Name = "DocumentDb"; +} + +public class DocumentDbFixture +{ + public IMongoClient Client { get; private set; } + + public IMongoDatabase Database => Client.GetDatabase($"Test_{Guid.NewGuid()}"); + + public DocumentDbFixture() + { + MongoTestUtils.SetupBson(); + + var settings = MongoClientSettings.FromConnectionString( + TestConfig.Configuration.GetValue("documentDb:configuration") + ); + + var cert = new X509Certificate2(TestConfig.Configuration.GetValue("documentDb:keyFile")!); + + settings.RetryWrites = false; + settings.RetryReads = false; + settings.SslSettings = new SslSettings + { + ClientCertificates = [cert], + CheckCertificateRevocation = false, + ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true, + }; + + Client = new MongoClient(settings); + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs index daa658abb..c534cc718 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs @@ -311,16 +311,16 @@ public abstract class TextIndexerTests : GivenContext [Fact] public async Task Should_delete_documents_from_index() { - await CreateTextAsync(Ids1[0], "iv", "Version1_1"); - await CreateTextAsync(Ids2[0], "iv", "Version2_1"); + await CreateTextAsync(Ids1[0], "iv", "Text1"); + await CreateTextAsync(Ids2[0], "iv", "Text2"); - await SearchText(expected: Ids1, text: "Version1_1"); - await SearchText(expected: Ids2, text: "Version2_1"); + await SearchText(expected: Ids1, text: "Text1"); + await SearchText(expected: Ids2, text: "Text2"); await DeleteAsync(Ids1[0]); - await SearchText(expected: null, text: "Version1_1"); - await SearchText(expected: Ids2, text: "Version2_1"); + await SearchText(expected: null, text: "Text1"); + await SearchText(expected: Ids2, text: "Text2"); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs index 8d9ba9c77..b093c708c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs @@ -67,6 +67,31 @@ public class RuleQueueWriterTests : GivenContext .MustNotHaveHappened(); } + [Fact] + public async Task Should_not_enqueue_result_with_skip_reason() + { + var result = new JobResult + { + SkipReason = SkipReason.FromRule, + EnrichedEvent = new EnrichedManualEvent(), + EnrichmentError = null, + Job = new CreateFlowInstanceRequest + { + Context = new FlowEventContext(), + Definition = new FlowDefinition(), + DefinitionId = Guid.NewGuid().ToString(), + OwnerId = Guid.NewGuid().ToString(), + }, + Rule = CreateRule(), + }; + + await sut.WriteAsync(AppId.Id, result); + await sut.FlushAsync(); + + A.CallTo(() => flowManager.EnqueueAsync(A[]>._, default)) + .MustNotHaveHappened(); + } + [Fact] public async Task Should_enqueue_success() { @@ -118,7 +143,7 @@ public class RuleQueueWriterTests : GivenContext { var result = new JobResult { - SkipReason = SkipReason.Disabled, + SkipReason = default, EnrichedEvent = new EnrichedManualEvent(), EnrichmentError = null, Job = new CreateFlowInstanceRequest diff --git a/frontend/src/app/features/api/api-area.component.html b/frontend/src/app/features/api/api-area.component.html index 1b1f13e09..742f8d850 100644 --- a/frontend/src/app/features/api/api-area.component.html +++ b/frontend/src/app/features/api/api-area.component.html @@ -9,13 +9,13 @@ diff --git a/frontend/src/app/features/content/pages/content/content-page.component.ts b/frontend/src/app/features/content/pages/content/content-page.component.ts index 25f5b51f3..d417b43d2 100644 --- a/frontend/src/app/features/content/pages/content/content-page.component.ts +++ b/frontend/src/app/features/content/pages/content/content-page.component.ts @@ -368,7 +368,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit { public loadVersion(version: number | null, compare: boolean) { const content = this.content; - if (!content || version === null || version != content.version) { + if (!content || version === null || version === content.version) { this.contentFormCompare = null; this.contentVersion = null; this.loadContent(content?.data || {}, true); diff --git a/frontend/src/app/features/content/shared/forms/field-editor.component.html b/frontend/src/app/features/content/shared/forms/field-editor.component.html index dce18ef06..b7867a1e9 100644 --- a/frontend/src/app/features/content/shared/forms/field-editor.component.html +++ b/frontend/src/app/features/content/shared/forms/field-editor.component.html @@ -56,7 +56,7 @@ (isExpandedChange)="toggleExpanded()" [language]="language" [languages]="languages" - [schemaIds]="field.rawProperties.schemaIds" + [schemaIds]="schemaIds" [url]="field.properties.editorUrl" /> } @else { @switch (field.properties.fieldType) { diff --git a/frontend/src/app/features/content/shared/forms/field-editor.component.ts b/frontend/src/app/features/content/shared/forms/field-editor.component.ts index 5633eed97..7c9044d7d 100644 --- a/frontend/src/app/features/content/shared/forms/field-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/field-editor.component.ts @@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common'; import { booleanAttribute, Component, ElementRef, EventEmitter, Input, numberAttribute, Output, ViewChild } from '@angular/core'; import { AbstractControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Observable } from 'rxjs'; -import { AbstractContentForm, AnnotationCreate, AnnotationsSelect, AnyFieldDto, AppLanguageDto, ChatDialogComponent, CheckboxGroupComponent, CodeEditorComponent, ColorPickerComponent, CommentsState, ConfirmClickDirective, ControlErrorsComponent, DateTimeEditorComponent, DialogModel, disabled$, EditContentForm, FormHintComponent, GeolocationEditorComponent, hasNoValue$, HTTP, IndeterminateValueDirective, MarkdownDirective, MathHelper, MessageBus, ModalDirective, RadioGroupComponent, ReferenceInputComponent, RichEditorComponent, StarsComponent, TagEditorComponent, ToggleComponent, TooltipDirective, TransformInputDirective, TypedSimpleChanges, Types } from '@app/shared'; +import { AbstractContentForm, AnnotationCreate, AnnotationsSelect, AnyFieldDto, AppLanguageDto, ChatDialogComponent, CheckboxGroupComponent, CodeEditorComponent, ColorPickerComponent, CommentsState, ConfirmClickDirective, ControlErrorsComponent, DateTimeEditorComponent, DialogModel, disabled$, EditContentForm, FormHintComponent, GeolocationEditorComponent, hasNoValue$, HTTP, IndeterminateValueDirective, MarkdownDirective, MathHelper, MessageBus, ModalDirective, RadioGroupComponent, ReferenceInputComponent, ReferencesFieldPropertiesDto, RichEditorComponent, StarsComponent, TagEditorComponent, ToggleComponent, TooltipDirective, TransformInputDirective, TypedSimpleChanges, Types } from '@app/shared'; import { ReferenceDropdownComponent } from '../references/reference-dropdown.component'; import { ReferencesCheckboxesComponent } from '../references/references-checkboxes.component'; import { ReferencesEditorComponent } from '../references/references-editor.component'; @@ -123,6 +123,10 @@ export class FieldEditorComponent { return this.field?.properties.fieldType === 'String'; } + public get schemaIds() { + return Types.is(this.field.properties, ReferencesFieldPropertiesDto) ? this.field.properties.schemaIds : undefined; + } + constructor( private readonly messageBus: MessageBus, ) { diff --git a/frontend/src/app/features/content/shared/forms/iframe-editor.component.ts b/frontend/src/app/features/content/shared/forms/iframe-editor.component.ts index 79fdfd72b..ea0027645 100644 --- a/frontend/src/app/features/content/shared/forms/iframe-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/iframe-editor.component.ts @@ -228,11 +228,11 @@ export class IFrameEditorComponent extends StatefulComponent implements O if (correlationId) { let schemaIds: ReadonlyArray | undefined = undefined; - if (this.schemaIds && this.schemaIds.length > 0) { - schemaIds = this.schemaIds; - } else if (Types.isArrayOfString(schemas) && schemas.length > 0) { + if (Types.isArrayOfString(schemas) && schemas.length > 0) { schemaIds = schemas; - } + } if (this.schemaIds && this.schemaIds.length > 0) { + schemaIds = this.schemaIds; + } this.contentsQuery = query; this.contentsCorrelationId = correlationId; diff --git a/frontend/src/app/features/dashboard/pages/cards/api-card.component.html b/frontend/src/app/features/dashboard/pages/cards/api-card.component.html index dce0412f1..d4557a3d2 100644 --- a/frontend/src/app/features/dashboard/pages/cards/api-card.component.html +++ b/frontend/src/app/features/dashboard/pages/cards/api-card.component.html @@ -3,7 +3,7 @@

- {{ "dashboard.contentApi" | sqxTranslate }} + {{ "dashboard.contentApi" | sqxTranslate }}

{{ "dashboard.contentApiDescription" | sqxTranslate }}