Browse Source

Add search fields.

pull/1294/head
Sebastian Stehle 2 months ago
parent
commit
3ab117ddc1
  1. 3
      backend/i18n/frontend_de.json
  2. 3
      backend/i18n/frontend_en.json
  3. 3
      backend/i18n/frontend_fr.json
  4. 3
      backend/i18n/frontend_it.json
  5. 3
      backend/i18n/frontend_nl.json
  6. 3
      backend/i18n/frontend_pt.json
  7. 3
      backend/i18n/frontend_zh.json
  8. 3
      backend/i18n/source/frontend_en.json
  9. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs
  10. 47
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  11. 28
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  12. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryVisitor.cs
  13. 13
      backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs
  14. 2
      backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs
  15. 3
      backend/src/Squidex.Infrastructure/Caching/QueryCache.cs
  16. 2
      backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs
  17. 6
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs
  18. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs
  19. 88
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs
  20. 31
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs
  21. 15
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaTests.cs
  22. 23854
      frontend/generator/Generator/cache.json
  23. 14
      frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html
  24. 34
      frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.ts
  25. 29
      frontend/src/app/features/schemas/pages/schema/fields/schema-fields.component.html
  26. 14
      frontend/src/app/features/schemas/pages/schema/fields/schema-fields.component.ts
  27. 58
      frontend/src/app/features/schemas/pages/schema/schema-page.component.html
  28. 5
      frontend/src/app/features/schemas/pages/schema/schema-page.component.ts
  29. 2
      frontend/src/app/framework/angular/forms/form-row.component.html
  30. 6
      frontend/src/app/shared/model/custom.ts
  31. 34
      frontend/src/app/shared/model/generated.ts
  32. 3
      frontend/src/app/shared/state/schemas.forms.ts

3
backend/i18n/frontend_de.json

@ -1048,6 +1048,9 @@
"schemas.schemaNameHint": "Sie können nur Buchstaben, Zahlen und Bindestriche verwenden und nicht mehr als 40 Zeichen.",
"schemas.schemaNameValidationMessage": "Der Name darf nur Buchstaben, Zahlen und Bindestriche enthalten.",
"schemas.schemaTagsHint": "Tags zur Annotation Ihres Schemas für Automatisierungsprozesse.",
"schemas.searchFields": "Search Fields",
"schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.",
"schemas.searchFieldsHint": "The search fields",
"schemas.searchPlaceholder": "Suchen",
"schemas.showFieldFailed": "Fehler beim Anzeigen des Feldes. Bitte neu laden.",
"schemas.synchronized": "Schema erfolgreich synchronisiert.",

3
backend/i18n/frontend_en.json

@ -1048,6 +1048,9 @@
"schemas.schemaNameHint": "You can only use letters, numbers and dashes and not more than 40 characters.",
"schemas.schemaNameValidationMessage": "Name can only contain letters, numbers and dashes.",
"schemas.schemaTagsHint": "Tags to annotate your schema for automation processes.",
"schemas.searchFields": "Search Fields",
"schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.",
"schemas.searchFieldsHint": "The search fields",
"schemas.searchPlaceholder": "Search",
"schemas.showFieldFailed": "Failed to show field. Please reload.",
"schemas.synchronized": "Schema synchronized successfully.",

3
backend/i18n/frontend_fr.json

@ -1048,6 +1048,9 @@
"schemas.schemaNameHint": "Vous ne pouvez utiliser que des lettres, des chiffres et des tirets et pas plus de 40 caractères.",
"schemas.schemaNameValidationMessage": "Le nom ne peut contenir que des lettres, des chiffres et des tirets.",
"schemas.schemaTagsHint": "Balises pour annoter votre schéma pour les processus d'automatisation.",
"schemas.searchFields": "Search Fields",
"schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.",
"schemas.searchFieldsHint": "The search fields",
"schemas.searchPlaceholder": "Recherche",
"schemas.showFieldFailed": "Impossible d'afficher le champ. Veuillez recharger.",
"schemas.synchronized": "Schéma synchronisé avec succès.",

3
backend/i18n/frontend_it.json

@ -1048,6 +1048,9 @@
"schemas.schemaNameHint": "Puoi utilizzare solo lettere, numeri e trattini e un numero massimo di 40 caratteri.",
"schemas.schemaNameValidationMessage": "Il nome può contenere solo lettere, numeri e trattini.",
"schemas.schemaTagsHint": "Tag per descrivere il tuo schema per i processi automatici.",
"schemas.searchFields": "Search Fields",
"schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.",
"schemas.searchFieldsHint": "The search fields",
"schemas.searchPlaceholder": "Cerca negli schemi...",
"schemas.showFieldFailed": "Non è stato possibile mostrare il campo. Per favore ricarica.",
"schemas.synchronized": "Lo Schema è stato sincronizzato con successo.",

3
backend/i18n/frontend_nl.json

@ -1048,6 +1048,9 @@
"schemas.schemaNameHint": "Je mag alleen letters, cijfers en streepjes gebruiken en niet meer dan 40 tekens.",
"schemas.schemaNameValidationMessage": "Naam mag alleen letters, cijfers en streepjes bevatten.",
"schemas.schemaTagsHint": "Tags om uw schema voor automatiseringsprocessen te annoteren.",
"schemas.searchFields": "Search Fields",
"schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.",
"schemas.searchFieldsHint": "The search fields",
"schemas.searchPlaceholder": "Zoek schema's ...",
"schemas.showFieldFailed": "Kan veld niet weergeven. Laad opnieuw.",
"schemas.synchronized": "Schema is succesvol gesynchroniseerd.",

3
backend/i18n/frontend_pt.json

@ -1048,6 +1048,9 @@
"schemas.schemaNameHint": "Só pode utilizar letras, números e traços e não mais de 40 caracteres.",
"schemas.schemaNameValidationMessage": "O nome só pode conter letras, números e traços.",
"schemas.schemaTagsHint": "Etiquetas para anotar o seu esquema para processos de automação.",
"schemas.searchFields": "Search Fields",
"schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.",
"schemas.searchFieldsHint": "The search fields",
"schemas.searchPlaceholder": "Pesquisar",
"schemas.showFieldFailed": "Falhou em mostrar o campo. Por favor, recarregue.",
"schemas.synchronized": "Esquema sincronizado com sucesso.",

3
backend/i18n/frontend_zh.json

@ -1048,6 +1048,9 @@
"schemas.schemaNameHint": "您只能使用字母、数字和破折号,并且不能超过 40 个字符。",
"schemas.schemaNameValidationMessage": "名称只能包含字母、数字和破折号。",
"schemas.schemaTagsHint": "用于注释自动化流程Schemas的标签。",
"schemas.searchFields": "Search Fields",
"schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.",
"schemas.searchFieldsHint": "The search fields",
"schemas.searchPlaceholder": "搜索Schemas...",
"schemas.showFieldFailed": "显示字段失败。请重新加载。",
"schemas.synchronized": "Schema synchronized successfully.",

3
backend/i18n/source/frontend_en.json

@ -1048,6 +1048,9 @@
"schemas.schemaNameHint": "You can only use letters, numbers and dashes and not more than 40 characters.",
"schemas.schemaNameValidationMessage": "Name can only contain letters, numbers and dashes.",
"schemas.schemaTagsHint": "Tags to annotate your schema for automation processes.",
"schemas.searchFields": "Search Fields",
"schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.",
"schemas.searchFieldsHint": "The search fields",
"schemas.searchPlaceholder": "Search",
"schemas.showFieldFailed": "Failed to show field. Please reload.",
"schemas.synchronized": "Schema synchronized successfully.",

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs

@ -15,6 +15,8 @@ public sealed record SchemaProperties : NamedElementPropertiesBase
public ReadonlyList<string>? Tags { get; init; }
public FieldNames? SearchFields { get; init; }
public string? ContentsSidebarUrl { get; init; }
public string? ContentSidebarUrl { get; init; }

47
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -34,7 +34,7 @@ public class ContentQueryParser(
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60);
private readonly ContentsOptions options = options.Value;
public virtual async Task<Q> ParseAsync(Context context, Q q, Schema? schema = null,
public virtual async Task<Q?> ParseAsync(Context context, Q q, Schema? schema = null,
CancellationToken ct = default)
{
Guard.NotNull(context);
@ -44,7 +44,11 @@ public class ContentQueryParser(
{
var query = await ParseClrQueryAsync(context, q, schema, ct);
await TransformFilterAsync(query, context, schema, ct);
var shouldCancel = await TransformFilterAsync(query, context, schema, ct);
if (shouldCancel)
{
return null;
}
WithSorting(query);
WithPaging(query, q);
@ -65,48 +69,71 @@ public class ContentQueryParser(
}
}
private async Task TransformFilterAsync(ClrQuery query, Context context, Schema? schema,
private async Task<bool> TransformFilterAsync(ClrQuery query, Context context, Schema? schema,
CancellationToken ct)
{
if (query.Filter != null && schema != null)
{
query.Filter = await GeoQueryTransformer.TransformAsync(query.Filter, context, schema, textIndex, ct);
query.Filter = await GeoQueryVisitor.VisitAsync(query.Filter, context, schema, textIndex, ct);
}
if (string.IsNullOrWhiteSpace(query.FullText))
{
return;
return false;
}
if (schema == null)
{
ThrowHelper.InvalidOperationException();
return;
return false;
}
var searchFilters = new List<CompareFilter<ClrValue>>();
var textQuery = new TextQuery(query.FullText, 1000)
{
PreferredSchemaId = schema.Id,
};
var fullTextIds = await textIndex.SearchAsync(context.App, textQuery, context.Scope(), ct);
var fullTextFilter = ClrFilter.Eq("id", "__notfound__");
if (fullTextIds is not { Count: > 0 } && schema.Properties.SearchFields is not { Count: > 0 })
{
// Cancel the search. We would not any results.
return true;
}
if (fullTextIds?.Count > 0)
{
fullTextFilter = ClrFilter.In("id", fullTextIds.Select(x => x.ToString()).ToList());
searchFilters.Add(ClrFilter.In("id", fullTextIds.Select(x => x.ToString()).ToList()));
}
foreach (var searchField in schema.Properties.SearchFields ?? FieldNames.Empty)
{
searchFilters.Add(ClrFilter.Contains(searchField, query.FullText));
}
if (searchFilters.Count == 0)
{
return false;
}
// Just an mini optimization to flatten OR expressions with one element.
var searchFilter =
searchFilters.Count == 1 ?
searchFilters[0] :
ClrFilter.Or(searchFilters) as FilterNode<ClrValue>;
if (query.Filter != null)
{
query.Filter = ClrFilter.And(query.Filter, fullTextFilter);
query.Filter = ClrFilter.And(query.Filter, searchFilter);
}
else
{
query.Filter = fullTextFilter;
query.Filter = searchFilter;
}
query.FullText = null;
return false;
}
private async Task<ClrQuery> ParseClrQueryAsync(Context context, Q q, Schema? schema,

28
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -113,13 +113,16 @@ public sealed class ContentQueryService(
q = q with { CreatedBy = context.UserPrincipal.Token() };
}
q = await ParseCoreAsync(context, q, schema, ct);
var contents = await QueryCoreAsync(context, q, schema, ct);
var parsedQuery = await ParseCoreAsync(context, q, schema, ct);
if (parsedQuery == null)
{
return ResultList.Empty<EnrichedContent>();
}
if (q.Ids is { Count: > 0 })
var contents = await QueryCoreAsync(context, parsedQuery, schema, ct);
if (parsedQuery.Ids is { Count: > 0 })
{
contents = contents.Sorted(x => x.Id, q.Ids);
contents = contents.Sorted(x => x.Id, parsedQuery.Ids);
}
return await TransformAsync(context, contents, ct);
@ -147,13 +150,16 @@ public sealed class ContentQueryService(
return ResultList.Empty<EnrichedContent>();
}
q = await ParseCoreAsync(context, q, null, ct);
var contents = await QueryCoreAsync(context, q, schemas, ct);
var parsedQuery = await ParseCoreAsync(context, q, null, ct);
if (parsedQuery == null)
{
return ResultList.Empty<EnrichedContent>();
}
if (q.Ids is { Count: > 0 })
var contents = await QueryCoreAsync(context, parsedQuery, schemas, ct);
if (parsedQuery.Ids is { Count: > 0 })
{
contents = contents.Sorted(x => x.Id, q.Ids);
contents = contents.Sorted(x => x.Id, parsedQuery.Ids);
}
return await TransformAsync(context, contents, ct);
@ -228,7 +234,7 @@ public sealed class ContentQueryService(
return schemas.Where(x => IsAccessible(x) && HasPermission(context, x, PermissionIds.AppContentsReadOwn)).ToList();
}
private async Task<Q> ParseCoreAsync(Context context, Q q, Schema? schema,
private async Task<Q?> ParseCoreAsync(Context context, Q q, Schema? schema,
CancellationToken ct)
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))

8
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryVisitor.cs

@ -13,17 +13,17 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.Contents.Queries;
internal sealed class GeoQueryTransformer : AsyncTransformVisitor<ClrValue, GeoQueryTransformer.Args>
internal sealed class GeoQueryVisitor : AsyncTransformVisitor<ClrValue, GeoQueryVisitor.Args>
{
public static readonly GeoQueryTransformer Instance = new GeoQueryTransformer();
public static readonly GeoQueryVisitor Instance = new GeoQueryVisitor();
public record struct Args(Context Context, Schema Schema, ITextIndex TextIndex, CancellationToken CancellationToken);
private GeoQueryTransformer()
private GeoQueryVisitor()
{
}
public static async Task<FilterNode<ClrValue>?> TransformAsync(FilterNode<ClrValue> filter, Context context, Schema schema, ITextIndex textIndex,
public static async Task<FilterNode<ClrValue>?> VisitAsync(FilterNode<ClrValue> filter, Context context, Schema schema, ITextIndex textIndex,
CancellationToken ct)
{
var args = new Args(context, schema, textIndex, ct);

13
backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs

@ -110,6 +110,19 @@ public static class GuardSchema
});
}
public static void CanUpdateSchema(UpdateSchema command)
{
Guard.NotNull(command);
Validate.It(e =>
{
if (command.Properties.SearchFields != null && command.Properties.SearchFields.Count > 3)
{
e(Not.Between("Size", 1, 3), "Properties.SearchFields");
}
});
}
private static void ValidateUpsert(IUpsertCommand command, AddValidation e)
{
if (command.Fields?.Length > 0)

2
backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs

@ -205,6 +205,8 @@ public partial class SchemaDomainObject(DomainId id, IPersistenceFactory<Schema>
case UpdateSchema update:
return ApplyReturn(update, c =>
{
GuardSchema.CanUpdateSchema(c);
Update(c);
return Snapshot;

3
backend/src/Squidex.Infrastructure/Caching/QueryCache.cs

@ -9,7 +9,8 @@ using Microsoft.Extensions.Caching.Memory;
namespace Squidex.Infrastructure.Caching;
public class QueryCache<TKey, T>(IMemoryCache? cacheStore = null, string? cacheKeyPrefix = null) : IQueryCache<TKey, T> where TKey : notnull
public class QueryCache<TKey, T>(IMemoryCache? cacheStore = null, string? cacheKeyPrefix = null)
: IQueryCache<TKey, T> where TKey : notnull
{
public void Set(TKey key, T item, TimeSpan cacheDuration)
{

2
backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs

@ -36,7 +36,6 @@ public static class QueryParser
var result = SimpleMapper.Map(query, new ClrQuery());
var errors = new List<string>();
model.ConvertSorting(result, errors);
model.ConvertFilters(result, errors, query);
@ -56,7 +55,6 @@ public static class QueryParser
}
var filter = JsonFilterVisitor.Parse(query.Filter, model, errors);
if (filter != null)
{
result.Filter = Optimizer<ClrValue>.Optimize(filter);

6
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Validation;
@ -54,6 +55,11 @@ public sealed class SchemaPropertiesDto
/// </summary>
public bool ValidateOnPublish { get; set; }
/// <summary>
/// The fields for automation processes.
/// </summary>
public FieldNames? SearchFields { get; init; }
/// <summary>
/// Tags for automation processes.
/// </summary>

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs

@ -49,6 +49,11 @@ public sealed class UpdateSchemaDto
/// </summary>
public bool ValidateOnPublish { get; set; }
/// <summary>
/// The fields for automation processes.
/// </summary>
public FieldNames? SearchFields { get; init; }
/// <summary>
/// Tags for automation processes.
/// </summary>

88
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs

@ -42,7 +42,7 @@ public class ContentQueryParserTests : GivenContext
{
var q = await sut.ParseAsync(ApiContext.Clone(b => b.WithNoTotal()), Q.Empty, ct: CancellationToken);
Assert.True(q.NoTotal);
Assert.True(q!.NoTotal);
}
[Fact]
@ -68,7 +68,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, ct: CancellationToken);
Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -78,7 +78,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, ct: CancellationToken);
Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: status == 'Draft'; Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -88,7 +88,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: status == 'Draft'; Take: 100; Sort: data.firstName.iv Ascending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: status == 'Draft'; Take: 100; Sort: data.firstName.iv Ascending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -98,7 +98,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 200; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -108,7 +108,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -121,7 +121,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: (data.firstName.iv == 'ABC' && id in ['1', '2']); Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: (data.firstName.iv == 'ABC' && id in ['1', '2']); Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -134,7 +134,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: id in ['1', '2']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: id in ['1', '2']; Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -147,11 +147,53 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: id in ['1']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: id in ['1']; Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
public async Task Should_convert_full_text_query_to_filter_if_index_returns_null()
public async Task Should_merge_results_of_full_text_and_search_Fields()
{
Schema = Schema with
{
Properties = Schema.Properties with
{
SearchFields = FieldNames.Create("a", "b"),
},
};
A.CallTo(() => textIndex.SearchAsync(ApiContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), ApiContext.Scope(), CancellationToken))
.Returns([DomainId.Create("1")]);
var query = Q.Empty.WithODataQuery("$search=Hello");
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: (id in ['1'] || contains(a, 'Hello') || contains(b, 'Hello')); Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
public async Task Should_only_use_search_fields_if_full_text_result_is_empty()
{
Schema = Schema with
{
Properties = Schema.Properties with
{
SearchFields = FieldNames.Create("a", "b"),
},
};
A.CallTo(() => textIndex.SearchAsync(ApiContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), ApiContext.Scope(), CancellationToken))
.Returns([]);
var query = Q.Empty.WithODataQuery("$search=Hello");
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: (contains(a, 'Hello') || contains(b, 'Hello')); Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
public async Task Should_return_null_if_full_index_returns_null()
{
A.CallTo(() => textIndex.SearchAsync(ApiContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), ApiContext.Scope(), CancellationToken))
.Returns(Task.FromResult<List<DomainId>?>(null));
@ -160,11 +202,11 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Null(q);
}
[Fact]
public async Task Should_convert_full_text_query_to_filter_if_index_returns_empty()
public async Task Should_return_null_if_full_index_returns_empty()
{
A.CallTo(() => textIndex.SearchAsync(ApiContext.App, A<TextQuery>.That.Matches(x => x.Text == "Hello"), ApiContext.Scope(), CancellationToken))
.Returns([]);
@ -173,7 +215,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Null(q);
}
[Fact]
@ -186,7 +228,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: id in ['1', '2']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: id in ['1', '2']; Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -199,7 +241,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: id in ['1']; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: id in ['1']; Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -212,7 +254,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -225,7 +267,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: id == '__notfound__'; Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Theory]
@ -239,7 +281,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -249,7 +291,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Take: 3; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Take: 3; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -259,7 +301,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Take: 20; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Take: 20; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -269,7 +311,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Skip: 20; Take: 200; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
[Fact]
@ -279,7 +321,7 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q.Query.ToString());
Assert.Equal("Skip: 20; Take: 200; Sort: id Descending", q!.Query.ToString());
}
[Fact]
@ -293,6 +335,6 @@ public class ContentQueryParserTests : GivenContext
var q = await sut.ParseAsync(ApiContext, query, Schema, CancellationToken);
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString());
Assert.Equal("Filter: data.firstName.iv == 'ABC'; Take: 30; Sort: lastModified Descending, id Ascending", q!.Query.ToString());
}
}

31
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Azure;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
@ -42,7 +43,7 @@ public class ContentQueryServiceTests : GivenContext
.Returns([Schema]);
A.CallTo(() => queryParser.ParseAsync(A<Context>._, A<Q>._, A<Schema?>._, CancellationToken))
.ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!));
.ReturnsLazily(c => Task.FromResult<Q?>(c.GetArgument<Q>(1)!));
var options = Options.Create(new ContentsOptions());
@ -265,6 +266,34 @@ public class ContentQueryServiceTests : GivenContext
.MustHaveHappened();
}
[Fact]
public async Task Should_not_query_contents_from_one_schema_if_query_cannot_be_parsed()
{
var requestContext = SetupContext(permissionId: PermissionIds.AppContentsRead);
A.CallTo(() => queryParser.ParseAsync(requestContext, A<Q>._, Schema, CancellationToken))
.Returns(Task.FromResult<Q?>(null));
await sut.QueryAsync(requestContext, SchemaId.Name, Q.Empty, CancellationToken);
A.CallTo(contentRepository)
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_query_contents_from_all_schemas_if_query_cannot_be_parsed()
{
var requestContext = SetupContext(permissionId: PermissionIds.AppContentsRead);
A.CallTo(() => queryParser.ParseAsync(requestContext, A<Q>._, null, CancellationToken))
.Returns(Task.FromResult<Q?>(null));
await sut.QueryAsync(requestContext, Q.Empty, CancellationToken);
A.CallTo(contentRepository)
.MustNotHaveHappened();
}
private void SetupEnricher()
{
A.CallTo(() => contentEnricher.EnrichAsync(A<IEnumerable<Content>>._, A<Context>._, CancellationToken))

15
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaTests.cs

@ -657,6 +657,21 @@ public class GuardSchemaTests : GivenContext, IClassFixture<TranslationsFixture>
GuardSchema.CanConfigurePreviewUrls(command);
}
[Fact]
public void CanUpdate_should_throw_exception_if_search_fields_is_too_large()
{
var command = new UpdateSchema
{
Properties = new SchemaProperties()
{
SearchFields = FieldNames.Create("a", "b", "c", "d"),
},
};
ValidationAssert.Throws(() => GuardSchema.CanUpdateSchema(command),
new ValidationError("Size must be between 1 and 3.", "Properties.SearchFields"));
}
private CreateSchema CreateCommand(CreateSchema command)
{
command.AppId = AppId;

23854
frontend/generator/Generator/cache.json

File diff suppressed because it is too large

14
frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html

@ -1,4 +1,4 @@
<form [formGroup]="fieldForm.form" (ngSubmit)="saveSchema()">
<form [formGroup]="schemaForm.form" (ngSubmit)="saveSchema()">
<h5>{{ "common.generalSettings" | sqxTranslate }}</h5>
<div class="card">
@ -31,17 +31,15 @@
<input class="form-control" id="contentsListUrl" formControlName="contentsListUrl" type="url" />
</sqx-form-row>
<sqx-form-row alert="schemas.searchFieldsHelp" for="searchFields" hint="schemas.searchFieldsHint" label="schemas.searchFields" vertical>
<sqx-tag-editor allowOpen="true" id="searchFields" formControlName="searchFields" [itemsSource]="fieldNames" />
</sqx-form-row>
<sqx-form-row for="tags" hint="schemas.schemaTagsHint" label="common.tags" vertical>
<sqx-tag-editor id="tags" formControlName="tags" />
</sqx-form-row>
<sqx-form-row
alert="schemas.validateOnPublishHint"
check
for="validateOnPublish"
hint="schemas.validateOnPublishHint"
label="schemas.validateOnPublish"
vertical>
<sqx-form-row check for="validateOnPublish" hint="schemas.validateOnPublishHint" label="schemas.validateOnPublish" vertical>
<input class="form-check-input" id="validateOnPublish" formControlName="validateOnPublish" type="checkbox" />
</sqx-form-row>
</div>

34
frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.ts

@ -8,7 +8,7 @@
import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { EditSchemaForm, FormRowComponent, SchemaDto, SchemasState, TagEditorComponent, TranslatePipe } from '@app/shared';
import { AppLanguageDto, EditSchemaForm, FormRowComponent, SchemaDto, SchemasState, TagEditorComponent, TranslatePipe } from '@app/shared';
@Component({
selector: 'sqx-schema-edit-form',
@ -26,10 +26,15 @@ export class SchemaEditFormComponent {
@Input({ required: true })
public schema!: SchemaDto;
public fieldForm = new EditSchemaForm();
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
public isEditable?: boolean | null;
public schemaForm = new EditSchemaForm();
public fieldNames: ReadonlyArray<string> = [];
constructor(
private readonly schemasState: SchemasState,
) {
@ -38,8 +43,23 @@ export class SchemaEditFormComponent {
public ngOnChanges() {
this.isEditable = this.schema.canUpdate;
this.fieldForm.load(this.schema.properties);
this.fieldForm.setEnabled(this.isEditable);
this.schemaForm.load(this.schema.properties);
this.schemaForm.setEnabled(this.isEditable);
const fieldNames = new Set<string>();
for (const field of this.schema.fields) {
if (field.properties.isContentField && field.properties.fieldType === 'String') {
if (field.partitioning === 'invariant') {
fieldNames.add(`data.${field.name}.iv`);
} else {
for (const language of this.languages) {
fieldNames.add(`data.${field.name}.${language}`);
}
}
}
}
this.fieldNames = [...fieldNames].sorted();
}
public saveSchema() {
@ -47,7 +67,7 @@ export class SchemaEditFormComponent {
return;
}
const value = this.fieldForm.submit();
const value = this.schemaForm.submit();
if (!value) {
return;
}
@ -55,10 +75,10 @@ export class SchemaEditFormComponent {
this.schemasState.update(this.schema, value)
.subscribe({
next: () => {
this.fieldForm.submitCompleted({ noReset: true });
this.schemaForm.submitCompleted({ noReset: true });
},
error: error => {
this.fieldForm.submitFailed(error);
this.schemaForm.submitFailed(error);
},
});
}

29
frontend/src/app/features/schemas/pages/schema/fields/schema-fields.component.html

@ -10,21 +10,20 @@
}
@if (appsState.selectedSettings | async; as settings) {
@if (languageState.isoLanguages | async; as languages) {
<sqx-sortable-field-list
[fields]="schema.fields"
[fieldsEmpty]="schema.fields.length === 0"
[languages]="languages"
[schema]="schema"
[settings]="settings"
[sortable]="schema.canOrderFields"
(sorted)="sortFields($event)" />
@if (schema.canAddField) {
<button class="btn btn-success field-button" (click)="fieldWizard.show()" type="button">
<i class="icon icon-plus field-button-icon"></i>
<div class="field-button-text">{{ "schemas.addFieldButton" | sqxTranslate }}</div>
</button>
}
<sqx-sortable-field-list
[fields]="schema.fields"
[fieldsEmpty]="schema.fields.length === 0"
[languages]="languages"
[schema]="schema"
[settings]="settings"
[sortable]="schema.canOrderFields"
(sorted)="sortFields($event)" />
@if (schema.canAddField) {
<button class="btn btn-success field-button" (click)="fieldWizard.show()" type="button">
<i class="icon icon-plus field-button-icon"></i>
<div class="field-button-text">{{ "schemas.addFieldButton" | sqxTranslate }}</div>
</button>
}
<sqx-field-wizard (dialogClose)="fieldWizard.hide()" [schema]="schema" [settings]="settings" *sqxModal="fieldWizard" />
}

14
frontend/src/app/features/schemas/pages/schema/fields/schema-fields.component.ts

@ -6,8 +6,8 @@
*/
import { AsyncPipe } from '@angular/common';
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { AppsState, DialogModel, FieldDto, fieldTypes, LanguagesState, ModalDirective, SchemaDto, SchemasState, TourStepDirective, TranslatePipe } from '@app/shared';
import { Component, forwardRef, Input } from '@angular/core';
import { AppLanguageDto, AppsState, DialogModel, FieldDto, fieldTypes, ModalDirective, SchemaDto, SchemasState, TourStepDirective, TranslatePipe } from '@app/shared';
import { FieldWizardComponent } from './field-wizard.component';
import { SortableFieldListComponent } from './sortable-field-list.component';
@ -24,24 +24,22 @@ import { SortableFieldListComponent } from './sortable-field-list.component';
forwardRef(() => SortableFieldListComponent),
],
})
export class SchemaFieldsComponent implements OnInit {
export class SchemaFieldsComponent {
@Input({ required: true })
public schema!: SchemaDto;
@Input({ required: true })
public languages!: AppLanguageDto[];
public fieldTypes = fieldTypes;
public fieldWizard = new DialogModel();
constructor(
public readonly appsState: AppsState,
public readonly schemasState: SchemasState,
public readonly languageState: LanguagesState,
) {
}
public ngOnInit() {
this.languageState.load();
}
public sortFields(fields: ReadonlyArray<FieldDto>) {
this.schemasState.orderFields(this.schema, fields).subscribe();
}

58
frontend/src/app/features/schemas/pages/schema/schema-page.component.html

@ -94,39 +94,41 @@
</sqx-dropdown-menu>
</ng-container>
<ng-container content>
@switch (tab) {
@case ("ui") {
<sqx-schema-ui-form [schema]="schema" />
}
@if (languageState.isoLanguages | async; as languages) {
@switch (tab) {
@case ("ui") {
<sqx-schema-ui-form [schema]="schema" />
}
@case ("scripts") {
<sqx-schema-scripts-form [schema]="schema" />
}
@case ("scripts") {
<sqx-schema-scripts-form [schema]="schema" />
}
@case ("json") {
<sqx-schema-export-form [schema]="schema" />
}
@case ("json") {
<sqx-schema-export-form [schema]="schema" />
}
@case ("indexes") {
<sqx-schema-indexes [schema]="schema" />
}
@case ("indexes") {
<sqx-schema-indexes [schema]="schema" />
}
@case ("more") {
<sqx-list-view innerWidth="50rem">
<div>
@if (schema.type !== "Component") {
<sqx-schema-preview-urls-form [schema]="schema" />
}
<sqx-schema-field-rules-form [schema]="schema" />
<sqx-schema-edit-form [schema]="schema" />
</div>
</sqx-list-view>
}
@case ("more") {
<sqx-list-view innerWidth="50rem">
<div>
@if (schema.type !== "Component") {
<sqx-schema-preview-urls-form [schema]="schema" />
}
<sqx-schema-field-rules-form [schema]="schema" />
<sqx-schema-edit-form [languages]="languages" [schema]="schema" />
</div>
</sqx-list-view>
}
@default {
<sqx-list-view innerWidth="50rem" table="true">
<div><sqx-schema-fields [schema]="schema" /></div>
</sqx-list-view>
@default {
<sqx-list-view innerWidth="50rem" table="true">
<div><sqx-schema-fields [languages]="languages" [schema]="schema" /></div>
</sqx-list-view>
}
}
}
</ng-container>

5
frontend/src/app/features/schemas/pages/schema/schema-page.component.ts

@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { map } from 'rxjs/operators';
import { ConfirmClickDirective, defined, DropdownMenuComponent, LayoutComponent, ListViewComponent, MessageBus, ModalDirective, ModalModel, ModalPlacementDirective, SchemaDto, SchemasState, SidebarMenuDirective, Subscriptions, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe, UIOptions } from '@app/shared';
import { ConfirmClickDirective, defined, DropdownMenuComponent, LanguagesState, LayoutComponent, ListViewComponent, MessageBus, ModalDirective, ModalModel, ModalPlacementDirective, SchemaDto, SchemasState, SidebarMenuDirective, Subscriptions, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe, UIOptions } from '@app/shared';
import { SchemaCloning } from '../messages';
import { SchemaEditFormComponent } from './common/schema-edit-form.component';
import { SchemaExportFormComponent } from './export/schema-export-form.component';
@ -62,6 +62,7 @@ export class SchemaPageComponent implements OnInit {
constructor(
public readonly schemasState: SchemasState,
public readonly languageState: LanguagesState,
private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly messageBus: MessageBus,
@ -69,6 +70,8 @@ export class SchemaPageComponent implements OnInit {
}
public ngOnInit() {
this.languageState.load();
this.subscriptions.add(
this.schemasState.selectedSchema.pipe(defined())
.subscribe(schema => {

2
frontend/src/app/framework/angular/forms/form-row.component.html

@ -37,6 +37,6 @@
}
@if (alert) {
<sqx-form-alert> {{ alert | sqxTranslate }} </sqx-form-alert>
<sqx-form-alert> <span [sqxMarkdown]="alert | sqxTranslate" inline="false"></span> </sqx-form-alert>
}
</div>

6
frontend/src/app/shared/model/custom.ts

@ -1,9 +1,9 @@
/* eslint-disable sort-imports */
import { hasAnyLink, StringHelper, Types, ApiUrlConfig, ErrorDto } from '@app/framework';
import { ApiUrlConfig, DateTime, ErrorDto, hasAnyLink, StringHelper, Types } from '@app/framework';
import * as generated from './generated';
import { FieldPropertiesVisitor, META_FIELDS, tableField, tableFields } from './schemas';
export const DUMMY = DateTime.now();
export class AppDto extends generated.AppDto {
public get displayName() {
return this.compute('displayName', () => StringHelper.firstNonEmpty(this.label, this.name));

34
frontend/src/app/shared/model/generated.ts

@ -8,7 +8,7 @@
/* eslint-disable */
// ReSharper disable InconsistentNaming
import { hasAnyLink, DateTime, StringHelper, Types, ApiUrlConfig, ErrorDto } from '@app/framework';
import { DateTime, hasAnyLink, StringHelper, Types, ApiUrlConfig, ErrorDto } from '@app/framework';
import { FieldPropertiesVisitor, META_FIELDS, tableField, tableFields } from './schemas';
export class ServerErrorDto implements IServerErrorDto {
@ -2607,6 +2607,8 @@ export class SchemaPropertiesDto implements ISchemaPropertiesDto {
readonly contentsListUrl?: string | undefined;
/** True to validate the content items on publish. */
readonly validateOnPublish!: boolean;
/** The fields for automation processes. */
readonly searchFields?: string[] | undefined;
/** Tags for automation processes. */
readonly tags?: string[] | undefined;
@ -2628,6 +2630,11 @@ export class SchemaPropertiesDto implements ISchemaPropertiesDto {
(<any>this).contentsEditorUrl = _data["contentsEditorUrl"];
(<any>this).contentsListUrl = _data["contentsListUrl"];
(<any>this).validateOnPublish = _data["validateOnPublish"];
if (Array.isArray(_data["searchFields"])) {
(<any>this).searchFields = [] as any;
for (let item of _data["searchFields"])
(<any>this).searchFields!.push(item);
}
if (Array.isArray(_data["tags"])) {
(<any>this).tags = [] as any;
for (let item of _data["tags"])
@ -2653,6 +2660,11 @@ export class SchemaPropertiesDto implements ISchemaPropertiesDto {
data["contentsEditorUrl"] = this.contentsEditorUrl;
data["contentsListUrl"] = this.contentsListUrl;
data["validateOnPublish"] = this.validateOnPublish;
if (Array.isArray(this.searchFields)) {
data["searchFields"] = [];
for (let item of this.searchFields)
data["searchFields"].push(item);
}
if (Array.isArray(this.tags)) {
data["tags"] = [];
for (let item of this.tags)
@ -2701,6 +2713,8 @@ export interface ISchemaPropertiesDto {
readonly contentsListUrl?: string | undefined;
/** True to validate the content items on publish. */
readonly validateOnPublish: boolean;
/** The fields for automation processes. */
readonly searchFields?: string[] | undefined;
/** Tags for automation processes. */
readonly tags?: string[] | undefined;
}
@ -5943,6 +5957,8 @@ export class UpdateSchemaDto implements IUpdateSchemaDto {
readonly contentsListUrl?: string | undefined;
/** True to validate the content items on publish. */
readonly validateOnPublish?: boolean;
/** The fields for automation processes. */
readonly searchFields?: string[] | undefined;
/** Tags for automation processes. */
readonly tags?: string[] | undefined;
@ -5962,6 +5978,11 @@ export class UpdateSchemaDto implements IUpdateSchemaDto {
(<any>this).contentSidebarUrl = _data["contentSidebarUrl"];
(<any>this).contentsListUrl = _data["contentsListUrl"];
(<any>this).validateOnPublish = _data["validateOnPublish"];
if (Array.isArray(_data["searchFields"])) {
(<any>this).searchFields = [] as any;
for (let item of _data["searchFields"])
(<any>this).searchFields!.push(item);
}
if (Array.isArray(_data["tags"])) {
(<any>this).tags = [] as any;
for (let item of _data["tags"])
@ -5985,6 +6006,11 @@ export class UpdateSchemaDto implements IUpdateSchemaDto {
data["contentSidebarUrl"] = this.contentSidebarUrl;
data["contentsListUrl"] = this.contentsListUrl;
data["validateOnPublish"] = this.validateOnPublish;
if (Array.isArray(this.searchFields)) {
data["searchFields"] = [];
for (let item of this.searchFields)
data["searchFields"].push(item);
}
if (Array.isArray(this.tags)) {
data["tags"] = [];
for (let item of this.tags)
@ -6029,6 +6055,8 @@ export interface IUpdateSchemaDto {
readonly contentsListUrl?: string | undefined;
/** True to validate the content items on publish. */
readonly validateOnPublish?: boolean;
/** The fields for automation processes. */
readonly searchFields?: string[] | undefined;
/** Tags for automation processes. */
readonly tags?: string[] | undefined;
}
@ -17703,8 +17731,8 @@ export interface FileResponse {
headers?: { [name: string]: any };
}
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable sort-imports */

3
frontend/src/app/shared/state/schemas.forms.ts

@ -421,6 +421,9 @@ export class EditSchemaForm extends Form<ExtendedFormGroup, UpdateSchemaDto, Sch
validateOnPublish: new UntypedFormControl(false,
Validators.nullValidator,
),
searchFields: new UntypedFormControl([],
Validators.nullValidator,
),
tags: new UntypedFormControl([],
Validators.nullValidator,
),

Loading…
Cancel
Save