Browse Source

Fix documentdb text (#1263)

* Update helm.

* Fix DocumentDB search.

* Fix tests

* Fix compare.

* Revert settings.

* Fixes
pull/1267/head
Sebastian Stehle 9 months ago
committed by GitHub
parent
commit
45e40dde15
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaFlowStep.cs
  2. 2
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchFlowStep.cs
  3. 36
      backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchFlowStep.cs
  4. 5
      backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseFlowStep.cs
  5. 2
      backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/CollectionProvider.cs
  6. 3
      backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/AtlasTextIndex.cs
  7. 179
      backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/DocumentDbTextIndex.cs
  8. 7
      backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndex.cs
  9. 4
      backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndexBase.cs
  10. 12
      backend/src/Squidex.Data.MongoDb/Infrastructure/MongoDbErrorCodes.cs
  11. 8
      backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs
  12. 4
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleQueueWriter.cs
  13. 11
      backend/src/Squidex/appsettings.json
  14. 37
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/DocumentDbTextIndexTests.cs
  15. 50
      backend/tests/Squidex.Data.Tests/MongoDb/TestHelpers/DocumentDbFixture.cs
  16. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs
  17. 27
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleQueueWriterTests.cs
  18. 4
      frontend/src/app/features/api/api-area.component.html
  19. 2
      frontend/src/app/features/content/pages/content/content-page.component.ts
  20. 2
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  21. 6
      frontend/src/app/features/content/shared/forms/field-editor.component.ts
  22. 8
      frontend/src/app/features/content/shared/forms/iframe-editor.component.ts
  23. 2
      frontend/src/app/features/dashboard/pages/cards/api-card.component.html

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

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

36
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<OpenSearchContent>(Document!);
}
catch (Exception ex)
{
content = new OpenSearchContent
{
More = new Dictionary<string, object>
{
["error"] = $"Invalid JSON: {ex.Message}",
},
};
}
Document = null;
return default;
}
Document = executionContext.SerializeJson(content);
OpenSearchContent content;
try
{
content = executionContext.DeserializeJson<OpenSearchContent>(Document!);
}
else
catch (Exception ex)
{
Document = null;
content = new OpenSearchContent
{
More = new Dictionary<string, object>
{
["error"] = $"Invalid JSON: {ex.Message}",
},
};
}
return base.PrepareAsync(executionContext, ct);
Document = executionContext.SerializeJson(content);
return default;
}
public override async ValueTask<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext,

5
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<FlowStepResult> ExecuteAsync(FlowExecutionContext executionContext,

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

3
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> atlasOptions, string shardKey) : MongoTextIndexBase<Dictionary<string, string>>(database, shardKey, new CommandFactory<Dictionary<string, string>>(BuildTexts))
public sealed class AtlasTextIndex(IMongoDatabase database, IHttpClientFactory atlasClient, IOptions<AtlasOptions> atlasOptions, string shardKey)
: MongoTextIndexBase<Dictionary<string, string>>(database, shardKey, new CommandFactory<Dictionary<string, string>>(BuildTexts))
{
private static readonly LuceneQueryVisitor QueryVisitor = new LuceneQueryVisitor(AtlasIndexDefinition.GetFieldPath);
private static readonly LuceneQueryAnalyzer QueryParser =

179
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<string>(database, shardKey, new CommandFactory<string>(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<MongoTextIndexEntity<string>> collection,
CancellationToken ct)
{
await collection.Indexes.CreateOneAsync(
new CreateIndexModel<MongoTextIndexEntity<string>>(
Index.Text(x => x.Texts)),
cancellationToken: ct);
await collection.Indexes.CreateManyAsync(
[
new CreateIndexModel<MongoTextIndexEntity<string>>(
Index
.Ascending(x => x.AppId)
.Ascending(x => x.ContentId)),
new CreateIndexModel<MongoTextIndexEntity<string>>(
Index
.Ascending(x => x.AppId)
.Ascending(x => x.SchemaId)
.Ascending(x => x.GeoField)
.Geo2DSphere(x => x.GeoObject)),
], ct);
}
public override async Task<List<DomainId>?> SearchAsync(App app, GeoQuery query, SearchScope scope,
CancellationToken ct = default)
{
Guard.NotNull(app);
Guard.NotNull(query);
var point = new GeoJsonPoint<GeoJson2DCoordinates>(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<MongoTextResult>(Projection.Include(x => x.ContentId))
.ToListAsync(ct);
return byGeo.Select(x => x.ContentId).ToList();
}
public override async Task<List<DomainId>?> 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<DomainId> 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<MongoTextIndexEntity<string>> filter, double factor,
CancellationToken ct)
{
var byText =
await GetCollection(search.SearchScope).Find(filter).Limit(search.Take)
.Project<MongoTextResult>(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<string, string> 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);
}
}
}

7
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<List<MongoTextIndexEntityText>>(database, shardKey, new CommandFactory<List<MongoTextIndexEntityText>>(BuildTexts))
public sealed class MongoTextIndex(IMongoDatabase database, string shardKey)
: MongoTextIndexBase<List<MongoTextIndexEntityText>>(database, shardKey, new CommandFactory<List<MongoTextIndexEntityText>>(BuildTexts))
{
private record struct SearchOperation
{
@ -33,9 +34,7 @@ public sealed class MongoTextIndex(IMongoDatabase database, string shardKey) : M
await collection.Indexes.CreateOneAsync(
new CreateIndexModel<MongoTextIndexEntity<List<MongoTextIndexEntityText>>>(
Index
.Ascending(x => x.AppId)
.Text("t.t")),
Index.Ascending(x => x.AppId).Text("t.t")),
cancellationToken: ct);
}

4
backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Text/MongoTextIndexBase.cs

@ -114,7 +114,9 @@ public abstract class MongoTextIndexBase<T>(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;
}

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

8
backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs

@ -228,6 +228,14 @@ public static class ServiceExtensions
shardKey => ActivatorUtilities.CreateInstance<AtlasTextIndex>(c, shardKey));
}).AsOptional<ITextIndex>().As<IDeleter>();
}
else if (config.GetValue<bool>("store:mongoDb:dpcumentDb"))
{
services.AddSingletonAs(c =>
{
return new MongoShardedTextIndex<string>(GetSharding(config, "store:mongoDB:textShardCount"),
shardKey => ActivatorUtilities.CreateInstance<DocumentDbTextIndex>(c, shardKey));
}).AsOptional<ITextIndex>().As<IDeleter>();
}
else
{
services.AddSingletonAs(c =>

4
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleQueueWriter.cs

@ -26,7 +26,9 @@ public sealed class RuleQueueWriter(IFlowManager<FlowEventContext> 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;
}

11
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": "<MY_KEY>",
"accessKey": "4U9IPATBRAZ2D0LOFS4P",
// The secret key for your user.
//
// Read More: https://supsystic.com/documentation/id-secret-access-key-amazon-s3/
"secretKey": "<MY_SECRET>",
"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": "",

37
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<ITextIndex> 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");
}
}

50
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<DocumentDbFixture>
{
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<string>("documentDb:configuration")
);
var cert = new X509Certificate2(TestConfig.Configuration.GetValue<string>("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);
}
}

12
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]

27
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<FlowEventContext>
{
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<CreateFlowInstanceRequest<FlowEventContext>[]>._, 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<FlowEventContext>

4
frontend/src/app/features/api/api-area.component.html

@ -9,13 +9,13 @@
<li class="nav-item nav-heading">{{ "common.openAPI" | sqxTranslate }}</li>
<li class="nav-item" sqxTourStep="contentApi">
<a class="nav-link" href="/api/content/{{ appsState.appName }}/docs" sqxExternalLink>
<a class="nav-link" href="api/content/{{ appsState.appName }}/docs" sqxExternalLink>
{{ "api.contentApi" | sqxTranslate }}
</a>
</li>
<li class="nav-item" sqxTourStep="generalAPi">
<a class="nav-link" href="/api/docs" sqxExternalLink> {{ "api.generalApi" | sqxTranslate }} </a>
<a class="nav-link" href="api/docs" sqxExternalLink> {{ "api.generalApi" | sqxTranslate }} </a>
</li>
</ul>
</ng-container>

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

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

6
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,
) {

8
frontend/src/app/features/content/shared/forms/iframe-editor.component.ts

@ -228,11 +228,11 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
if (correlationId) {
let schemaIds: ReadonlyArray<string> | 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;

2
frontend/src/app/features/dashboard/pages/cards/api-card.component.html

@ -3,7 +3,7 @@
<div class="card-image"><img src="./images/dashboard-api.svg" /></div>
<h4 class="card-title">
<a href="/api/content/{{ app.name }}/docs" sqxExternalLink>{{ "dashboard.contentApi" | sqxTranslate }}</a>
<a href="api/content/{{ app.name }}/docs" sqxExternalLink>{{ "dashboard.contentApi" | sqxTranslate }}</a>
</h4>
<div class="card-text">{{ "dashboard.contentApiDescription" | sqxTranslate }}</div>

Loading…
Cancel
Save