Browse Source

Provide translation status. (#916)

* Provide translation status.

* Fix tests.

* Fix average calculation.
pull/917/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
0acdbeec5d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 68
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs
  2. 18
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs
  3. 6
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx
  4. 17
      backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs
  5. 23
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/ContentQueryModel.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs
  7. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
  8. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs
  9. 16
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  10. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  11. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs
  12. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs
  13. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs
  14. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentUnionGraphType.cs
  15. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs
  16. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ReferenceUnionGraphType.cs
  17. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs
  18. 2
      backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs
  19. 5
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  20. 4
      backend/src/Squidex.Infrastructure/Language.cs
  21. 16
      backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs
  22. 2
      backend/src/Squidex.Web/ApiExceptionConverter.cs
  23. 2
      backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs
  24. 103
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/TranslationStatusTests.cs
  25. 26
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs
  26. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs
  27. 42
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs
  28. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs
  29. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs
  30. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs
  31. 15
      backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs
  32. 12
      frontend/src/app/features/content/pages/content/content-page.component.html
  33. 4
      frontend/src/app/features/content/pages/content/editor/content-field.component.html
  34. 7
      frontend/src/app/features/content/pages/content/editor/field-languages.component.html
  35. 6
      frontend/src/app/features/content/pages/content/editor/field-languages.component.ts
  36. 3
      frontend/src/app/features/content/pages/contents/contents-page.component.html
  37. 8
      frontend/src/app/features/content/pages/contents/contents-page.component.ts
  38. 8
      frontend/src/app/features/content/shared/references/content-creator.component.html
  39. 2
      frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts
  40. 6
      frontend/src/app/framework/angular/forms/forms-helper.ts
  41. 34
      frontend/src/app/framework/angular/language-selector.component.html
  42. 21
      frontend/src/app/framework/angular/language-selector.component.scss
  43. 19
      frontend/src/app/framework/angular/language-selector.component.ts
  44. 135
      frontend/src/app/framework/angular/language-selector.stories.tsx
  45. 1
      frontend/src/app/framework/utils/modal-positioner.ts
  46. 61
      frontend/src/app/shared/state/contents.forms-helpers.ts
  47. 155
      frontend/src/app/shared/state/contents.forms.spec.ts
  48. 10
      frontend/src/app/shared/state/contents.forms.ts

68
backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs

@ -0,0 +1,68 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Contents
{
public class TranslationStatus : Dictionary<string, int>
{
public TranslationStatus()
{
}
public TranslationStatus(int capacity)
: base(capacity)
{
}
public static TranslationStatus Create(ContentData data, Schema schema, LanguagesConfig languages)
{
Guard.NotNull(data);
Guard.NotNull(schema);
Guard.NotNull(languages);
var result = new TranslationStatus(languages.Languages.Count);
var localizedFields = schema.Fields.Where(x => x.Partitioning == Partitioning.Language).ToList();
foreach (var language in languages.AllKeys)
{
var percent = 0;
foreach (var field in localizedFields)
{
if (IsValidValue(data.GetValueOrDefault(field.Name)?.GetValueOrDefault(language)))
{
percent++;
}
}
if (localizedFields.Count > 0)
{
percent = (int)Math.Round(100 * (double)percent / localizedFields.Count);
}
else
{
percent = 100;
}
result[language] = percent;
}
return result;
}
private static bool IsValidValue(JsonValue? value)
{
return value != null && value.Value.Type != JsonValueType.Null;
}
}
}

18
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs

@ -888,6 +888,24 @@ namespace Squidex.Domain.Apps.Core {
}
}
/// <summary>
/// Looks up a localized string similar to The translation status..
/// </summary>
public static string TranslationStatus {
get {
return ResourceManager.GetString("TranslationStatus", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The translation status ({0})..
/// </summary>
public static string TranslationStatusLanguage {
get {
return ResourceManager.GetString("TranslationStatusLanguage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The current number of calls..
/// </summary>

6
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx

@ -393,6 +393,12 @@
<data name="StringFieldText" xml:space="preserve">
<value>The text of this field.</value>
</data>
<data name="TranslationStatus" xml:space="preserve">
<value>The translation status.</value>
</data>
<data name="TranslationStatusLanguage" xml:space="preserve">
<value>The translation status ({0}).</value>
</data>
<data name="UsageCallsCurrent" xml:space="preserve">
<value>The current number of calls.</value>
</data>

17
backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Core
{
public delegate IFieldPartitioning PartitionResolver(Partitioning key);
public sealed class Partitioning : IEquatable<Partitioning>
public sealed record Partitioning
{
public static readonly Partitioning Invariant = new Partitioning("invariant");
public static readonly Partitioning Language = new Partitioning("language");
@ -27,21 +27,6 @@ namespace Squidex.Domain.Apps.Core
Key = key;
}
public override bool Equals(object? obj)
{
return Equals(obj as Partitioning);
}
public bool Equals(Partitioning? other)
{
return string.Equals(other?.Key, Key, StringComparison.OrdinalIgnoreCase);
}
public override int GetHashCode()
{
return Key.GetHashCode(StringComparison.Ordinal);
}
public override string ToString()
{
return Key;

23
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/ContentQueryModel.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Globalization;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Queries;
@ -55,6 +56,10 @@ namespace Squidex.Domain.Apps.Core.GenerateFilters
}
};
var translationStatusSchema = BuildTranslationStatus(partitionResolver);
fields.Add(new FilterField(translationStatusSchema, "translationStatus"));
if (schema != null)
{
var dataSchema = schema.BuildDataSchema(partitionResolver, components);
@ -72,5 +77,23 @@ namespace Squidex.Domain.Apps.Core.GenerateFilters
return new QueryModel { Schema = filterSchema };
}
private static FilterSchema BuildTranslationStatus(PartitionResolver partitionResolver)
{
var fields = new List<FilterField>();
foreach (var key in partitionResolver(Partitioning.Language).AllKeys)
{
fields.Add(new FilterField(FilterSchema.Number, key)
{
Description = string.Format(CultureInfo.InvariantCulture, FieldDescriptions.TranslationStatusLanguage, key)
});
}
return new FilterSchema(FilterSchemaType.Object)
{
Fields = fields.ToReadonlyList()
};
}
}
}

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs

@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
Guard.NotNull(typeName);
return schemas.Value.GetOrDefault(typeName);
return schemas.Value.GetValueOrDefault(typeName);
}
private Dictionary<string, JsonSchema> GenerateSchemas()

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs

@ -129,7 +129,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
return PropertyDescriptor.Undefined;
}
return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined;
return valueProperties?.GetValueOrDefault(propertyName) ?? PropertyDescriptor.Undefined;
}
public override IEnumerable<KeyValuePair<JsValue, PropertyDescriptor>> GetOwnProperties()

8
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/ContentFluidExtension.cs

@ -48,20 +48,20 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions
{
if (value.Value is JsonObject o)
{
return o.GetOrDefault(name);
return o.GetValueOrDefault(name);
}
return null;
});
memberAccessStrategy.Register<ContentData, object?>(
(value, name) => value.GetOrDefault(name));
(value, name) => value.GetValueOrDefault(name));
memberAccessStrategy.Register<ContentFieldData, object?>(
(value, name) => value.GetOrDefault(name).Value);
(value, name) => value.GetValueOrDefault(name).Value);
memberAccessStrategy.Register<JsonObject, object?>(
(value, name) => value.GetOrDefault(name).Value);
(value, name) => value.GetValueOrDefault(name).Value);
}
}
}

16
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs

@ -100,6 +100,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonElement("mb")]
public RefToken LastModifiedBy { get; set; }
[BsonIgnoreIfNull]
[BsonElement("ts")]
public TranslationStatus? TranslationStatus { get; set; }
public DomainId UniqueId
{
get => DocumentId;
@ -134,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return entity;
}
public static async Task<MongoContentEntity> CreateAsync(SnapshotWriteJob<ContentDomainObject.State> job, IAppProvider appProvider)
public static async Task<MongoContentEntity> CreateCompleteAsync(SnapshotWriteJob<ContentDomainObject.State> job, IAppProvider appProvider)
{
var entity = await CreateContentAsync(job.Value.Data, job, appProvider);
@ -158,16 +162,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
entity.ReferencedIds ??= new HashSet<DomainId>();
entity.Version = job.NewVersion;
if (data.CanHaveReference())
{
var schema = await appProvider.GetSchemaAsync(job.Value.AppId.Id, job.Value.SchemaId.Id, true);
var (app, schema) = await appProvider.GetAppWithSchemaAsync(job.Value.AppId.Id, job.Value.SchemaId.Id, true);
if (schema != null)
if (schema?.SchemaDef != null && app != null)
{
if (data.CanHaveReference())
{
var components = await appProvider.GetComponentsAsync(schema);
entity.Data.AddReferencedIds(schema.SchemaDef, entity.ReferencedIds, components);
}
entity.TranslationStatus = TranslationStatus.Create(data, schema.SchemaDef, app.Languages);
}
return entity;

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs

@ -139,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
if (isValid)
{
await collectionComplete.AddCollectionsAsync(
await MongoContentEntity.CreateAsync(job, appProvider), add, ct);
await MongoContentEntity.CreateCompleteAsync(job, appProvider), add, ct);
}
}
@ -191,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private async Task UpsertCompleteAsync(SnapshotWriteJob<ContentDomainObject.State> job,
CancellationToken ct)
{
var entityJob = job.As(await MongoContentEntity.CreateAsync(job, appProvider));
var entityJob = job.As(await MongoContentEntity.CreateCompleteAsync(job, appProvider));
await collectionComplete.UpsertAsync(entityJob, ct);
}
@ -199,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private async Task UpsertVersionedCompleteAsync(IClientSessionHandle session, SnapshotWriteJob<ContentDomainObject.State> job,
CancellationToken ct)
{
var entityJob = job.As(await MongoContentEntity.CreateAsync(job, appProvider));
var entityJob = job.As(await MongoContentEntity.CreateCompleteAsync(job, appProvider));
await collectionComplete.UpsertVersionedAsync(session, entityJob, ct);
}

4
backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs

@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
public IAppLimitsPlan? GetPlan(string? planId)
{
return plansById.GetOrDefault(planId ?? string.Empty);
return plansById.GetValueOrDefault(planId ?? string.Empty);
}
public IAppLimitsPlan GetFreePlan()
@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
private ConfigAppLimitsPlan GetPlanCore(string? planId)
{
return plansById.GetOrDefault(planId ?? string.Empty) ?? freePlan;
return plansById.GetValueOrDefault(planId ?? string.Empty) ?? freePlan;
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs

@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
foreach (var tag in tagIds)
{
perApp[tag] = perApp.GetOrDefault(tag) + count;
perApp[tag] = perApp.GetValueOrDefault(tag) + count;
}
}
}

6
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs

@ -136,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public IObjectGraphType GetContentResultType(SchemaInfo schemaId)
{
return contentResultTypes.GetOrDefault(schemaId);
return contentResultTypes.GetValueOrDefault(schemaId)!;
}
public IObjectGraphType? GetContentType(DomainId schemaId)
@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public IObjectGraphType GetContentType(SchemaInfo schemaId)
{
return contentTypes.GetOrDefault(schemaId);
return contentTypes.GetValueOrDefault(schemaId)!;
}
public IObjectGraphType? GetComponentType(DomainId schemaId)
@ -158,7 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return null;
}
return componentTypes.GetOrDefault(schema);
return componentTypes.GetValueOrDefault(schema);
}
public EmbeddableStringGraphType GetEmbeddableString(FieldInfo fieldInfo, StringFieldProperties properties)

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentUnionGraphType.cs

@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
if (value is JsonObject json && Component.IsValid(json, out var schemaId))
{
return types.GetOrDefault(schemaId);
return types.GetValueOrDefault(schemaId);
}
return null;

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
var fieldName = fieldContext.FieldDefinition.SourceName();
return content?.GetOrDefault(fieldName);
return content?.GetValueOrDefault(fieldName);
});
public static readonly IFieldResolver Url = Resolve((content, _, context) =>

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ReferenceUnionGraphType.cs

@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
if (value is IContentEntity content)
{
return types.GetOrDefault(content.SchemaId.Id);
return types.GetValueOrDefault(content.SchemaId.Id);
}
return null;

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs

@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.State
foreach (var id in missingIds)
{
var state = fromInner.GetOrDefault(id);
var state = fromInner.GetValueOrDefault(id);
cache.Set(id, Tuple.Create<TextContentState?>(state));
}

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

@ -230,7 +230,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards
{
var fieldPrefix = $"{path}[{fieldIndex}]";
var field = schema.FieldsByName.GetOrDefault(fieldName ?? string.Empty);
var field = schema.FieldsByName.GetValueOrDefault(fieldName ?? string.Empty);
if (string.IsNullOrWhiteSpace(fieldName))
{

5
backend/src/Squidex.Infrastructure/CollectionExtensions.cs

@ -317,11 +317,6 @@ namespace Squidex.Infrastructure
return dictionary.ToDictionary(x => x.Key, x => x.Value);
}
public static TValue GetOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key) where TKey : notnull
{
return dictionary.GetOrCreate(key, _ => default!);
}
public static TValue GetOrAddDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key) where TKey : notnull
{
return dictionary.GetOrAdd(key, _ => default!);

4
backend/src/Squidex.Infrastructure/Language.cs

@ -37,12 +37,12 @@ namespace Squidex.Infrastructure
public string EnglishName
{
get => NamesEnglish.GetOrDefault(Iso2Code) ?? string.Empty;
get => NamesEnglish.GetValueOrDefault(Iso2Code) ?? string.Empty;
}
public string NativeName
{
get => NamesNative.GetOrDefault(Iso2Code) ?? string.Empty;
get => NamesNative.GetValueOrDefault(Iso2Code) ?? string.Empty;
}
private Language(string iso2Code)

16
backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs

@ -115,28 +115,24 @@ namespace Squidex.Infrastructure.Reflection
return GetName(typeof(T));
}
public string GetNameOrNull<T>()
public string? GetNameOrNull<T>()
{
return GetNameOrNull(typeof(T));
}
public string GetNameOrNull(Type type)
public string? GetNameOrNull(Type type)
{
var result = namesByType.GetOrDefault(type);
return result;
return namesByType.GetValueOrDefault(type);
}
public Type? GetTypeOrNull(string name)
{
var result = typesByName.GetOrDefault(name);
return result;
return typesByName.GetValueOrDefault(name);
}
public string GetName(Type type)
{
var result = namesByType.GetOrDefault(type);
var result = namesByType.GetValueOrDefault(type);
if (result == null)
{
@ -148,7 +144,7 @@ namespace Squidex.Infrastructure.Reflection
public Type GetType(string name)
{
var result = typesByName.GetOrDefault(name);
var result = typesByName.GetValueOrDefault(name);
if (result == null)
{

2
backend/src/Squidex.Web/ApiExceptionConverter.cs

@ -73,7 +73,7 @@ namespace Squidex.Web
error.StatusCode = 500;
}
error.Type = Links.GetOrDefault(error.StatusCode);
error.Type = Links.GetValueOrDefault(error.StatusCode);
}
private static (ErrorDto Error, Exception? Unhandled) CreateError(Exception exception)

2
backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs

@ -67,7 +67,7 @@ namespace Squidex.Areas.IdentityServer.Config
{
var app = await appProvider.GetAppAsync(appName, true);
var appClient = app?.Clients.GetOrDefault(appClientId);
var appClient = app?.Clients.GetValueOrDefault(appClientId);
if (appClient != null)
{

103
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/TranslationStatusTests.cs

@ -0,0 +1,103 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Core.Model.Contents
{
public class TranslationStatusTests
{
private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE).Set(Language.IT);
[Fact]
public void Should_create_info_for_empty_schema()
{
var schema = new Schema("my-schema");
var result = TranslationStatus.Create(new ContentData(), schema, languages);
Assert.Equal(new TranslationStatus
{
[Language.EN] = 100,
[Language.DE] = 100,
[Language.IT] = 100
}, result);
}
[Fact]
public void Should_create_info_for_schema_without_localized_field()
{
var schema =
new Schema("my-schema")
.AddString(1, "field1", Partitioning.Invariant);
var result = TranslationStatus.Create(new ContentData(), schema, languages);
Assert.Equal(new TranslationStatus
{
[Language.EN] = 100,
[Language.DE] = 100,
[Language.IT] = 100
}, result);
}
[Fact]
public void Should_create_info_for_schema_with_localized_field()
{
var schema =
new Schema("my-schema")
.AddString(1, "field1", Partitioning.Language);
var result = TranslationStatus.Create(new ContentData(), schema, languages);
Assert.Equal(new TranslationStatus
{
[Language.EN] = 0,
[Language.DE] = 0,
[Language.IT] = 0
}, result);
}
[Fact]
public void Should_create_translation_info()
{
var schema =
new Schema("my-schema")
.AddString(1, "field1", Partitioning.Language)
.AddString(2, "field2", Partitioning.Language)
.AddString(3, "field3", Partitioning.Language)
.AddString(4, "field4", Partitioning.Invariant);
var data =
new ContentData()
.AddField("field1",
new ContentFieldData()
.AddLocalized(Language.EN, "en")
.AddLocalized(Language.DE, "de"))
.AddField("field2",
new ContentFieldData()
.AddLocalized(Language.EN, "en")
.AddLocalized(Language.DE, "de"))
.AddField("field3",
new ContentFieldData()
.AddLocalized(Language.EN, "en"));
var result = TranslationStatus.Create(data, schema, languages);
Assert.Equal(new TranslationStatus
{
[Language.EN] = 100,
[Language.DE] = 67,
[Language.IT] = 0
}, result);
}
}
}

26
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
{
public class FieldConvertersTests
{
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE);
private static IEnumerable<object?[]> InvalidValues()
{
@ -181,7 +181,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var result =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new ResolveLanguages(languagesConfig))
.Add(new ResolveLanguages(languages))
.Convert(source);
var expected =
@ -213,7 +213,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var result =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new ResolveLanguages(languagesConfig, true, new[] { Language.DE }))
.Add(new ResolveLanguages(languages, true, new[] { Language.DE }))
.Convert(source);
var expected =
@ -249,7 +249,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var result =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new ResolveLanguages(languagesConfig))
.Add(new ResolveLanguages(languages))
.Convert(source);
var expected =
@ -285,7 +285,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var result =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new ResolveLanguages(languagesConfig))
.Add(new ResolveLanguages(languages))
.Convert(source);
var expected =
@ -318,7 +318,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var result =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new ResolveLanguages(languagesConfig))
.Add(new ResolveLanguages(languages))
.Convert(source);
var expected = source;
@ -350,7 +350,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var result =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new ResolveInvariant(languagesConfig))
.Add(new ResolveInvariant(languages))
.Convert(source);
var expected =
@ -386,7 +386,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var result =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new ResolveInvariant(languagesConfig))
.Add(new ResolveInvariant(languages))
.Convert(source);
var expected =
@ -414,7 +414,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var result =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new ResolveLanguages(languagesConfig))
.Add(new ResolveLanguages(languages))
.Convert(source);
var expected = source;
@ -485,7 +485,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var result =
new ContentConverter(ResolvedComponents.Empty, schema)
.Add(new ResolveLanguages(languagesConfig, true, Language.IT))
.Add(new ResolveLanguages(languages, true, Language.IT))
.Convert(source);
var expected =
@ -505,7 +505,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source = new ContentFieldData();
var result =
new ResolveLanguages(languagesConfig)
new ResolveLanguages(languages)
.ConvertFieldAfter(field, source);
Assert.Same(source, result);
@ -519,7 +519,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source = new ContentFieldData();
var result =
new ResolveLanguages(languagesConfig, true, Array.Empty<Language>())
new ResolveLanguages(languages, true, Array.Empty<Language>())
.ConvertFieldAfter(field, source);
Assert.Same(source, result);
@ -533,7 +533,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source = new ContentFieldData();
var result =
new ResolveLanguages(languagesConfig)
new ResolveLanguages(languages)
.ConvertFieldAfter(field, source);
Assert.Same(source, result);

6
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues
public class DefaultValuesTests
{
private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10);
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE);
private readonly Language language = Language.DE;
private readonly Schema schema;
@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues
new ContentFieldData()
.AddInvariant(456));
data.GenerateDefaultValues(schema, languagesConfig.ToResolver());
data.GenerateDefaultValues(schema, languages.ToResolver());
Assert.Equal(456, data["myNumber"]!["iv"].AsNumber);
@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues
new ContentFieldData()
.AddInvariant(456));
data.GenerateDefaultValues(schema, languagesConfig.ToResolver());
data.GenerateDefaultValues(schema, languages.ToResolver());
Assert.Equal(string.Empty, data["myString"]!["de"].AsString);
Assert.Equal("en-string", data["myString"]!["en"].AsString);

42
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs

@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
public class ContentValidationTests : IClassFixture<TranslationsFixture>
{
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE);
private readonly List<ValidationError> errors = new List<ValidationError>();
private Schema schema = new Schema("my-schema");
@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new ContentFieldData()
.AddInvariant(1000));
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema, factory: validatorFactory);
await data.ValidateAsync(languages.ToResolver(), errors, schema, factory: validatorFactory);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new ContentFieldData()
.AddInvariant(1000));
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema, factory: validatorFactory);
await data.ValidateAsync(languages.ToResolver(), errors, schema, factory: validatorFactory);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddField("unknown",
new ContentFieldData());
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidateAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -116,7 +116,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new ContentFieldData()
.AddInvariant(1000));
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidateAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddLocalized("es", 1)
.AddLocalized("it", 1));
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidateAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new ContentData();
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidateAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -175,7 +175,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new ContentData();
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidateAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -193,7 +193,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new ContentData();
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidateAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -214,7 +214,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddLocalized("de", 1)
.AddLocalized("ru", 1));
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidateAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -258,7 +258,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddLocalized("es", 1)
.AddLocalized("it", 1));
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidateAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -276,7 +276,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddField("unknown",
new ContentFieldData());
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidatePartialAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -297,7 +297,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new ContentFieldData()
.AddInvariant(1000));
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidatePartialAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -318,7 +318,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddLocalized("es", 1)
.AddLocalized("it", 1));
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidatePartialAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -337,7 +337,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new ContentData();
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidatePartialAsync(languages.ToResolver(), errors, schema);
Assert.Empty(errors);
}
@ -351,7 +351,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new ContentData();
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidatePartialAsync(languages.ToResolver(), errors, schema);
Assert.Empty(errors);
}
@ -368,7 +368,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddLocalized("de", 1)
.AddLocalized("ru", 1));
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidatePartialAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -389,7 +389,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddLocalized("es", 1)
.AddLocalized("it", 1));
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidatePartialAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -415,7 +415,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new JsonObject().Add("myNested", 1),
new JsonObject())));
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidatePartialAsync(languages.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -433,7 +433,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new ContentData();
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidateAsync(languages.ToResolver(), errors, schema);
Assert.Empty(errors);
}
@ -452,7 +452,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
JsonValue.Array(
new JsonObject())));
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
await data.ValidateAsync(languages.ToResolver(), errors, schema);
Assert.Empty(errors);
}

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentMappingTests.cs

@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
var source = CreateContentWithoutNewVersion();
var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version);
var snapshot = await MongoContentEntity.CreateAsync(snapshotJob, appProvider);
var snapshot = await MongoContentEntity.CreateCompleteAsync(snapshotJob, appProvider);
Assert.Equal(source.CurrentVersion.Data, snapshot.Data);
Assert.Null(snapshot.DraftData);
@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
var source = CreateContentWithNewVersion();
var snapshotJob = new SnapshotWriteJob<ContentDomainObject.State>(source.UniqueId, source, source.Version);
var snapshot = await MongoContentEntity.CreateAsync(snapshotJob, appProvider);
var snapshot = await MongoContentEntity.CreateCompleteAsync(snapshotJob, appProvider);
Assert.Equal(source.NewVersion?.Data, snapshot.Data);
Assert.Equal(source.CurrentVersion.Data, snapshot.DraftData);

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentQueryTests.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
{
private readonly DomainId appId = DomainId.NewGuid();
private readonly Schema schemaDef;
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE);
static ContentQueryTests()
{
@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
var app = A.Dummy<IAppEntity>();
A.CallTo(() => app.Id).Returns(DomainId.NewGuid());
A.CallTo(() => app.Version).Returns(3);
A.CallTo(() => app.Languages).Returns(languagesConfig);
A.CallTo(() => app.Languages).Returns(languages);
}
[Fact]

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs

@ -755,12 +755,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject
private IField GetField(int id)
{
return sut.Snapshot.SchemaDef.FieldsById.GetOrDefault(id);
return sut.Snapshot.SchemaDef.FieldsById.GetValueOrDefault(id)!;
}
private IField GetNestedField(int parentId, int childId)
{
return ((IArrayField)sut.Snapshot.SchemaDef.FieldsById[parentId]).FieldsById.GetOrDefault(childId);
return ((IArrayField)sut.Snapshot.SchemaDef.FieldsById[parentId]).FieldsById.GetValueOrDefault(childId)!;
}
private static StringFieldProperties ValidProperties()

15
backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs

@ -62,21 +62,6 @@ namespace Squidex.Infrastructure
Assert.Equal(-1, index);
}
[Fact]
public void GetOrDefault_should_return_value_if_key_exists()
{
valueDictionary[12] = 34;
Assert.Equal(34, valueDictionary.GetOrDefault(12));
}
[Fact]
public void GetOrDefault_should_return_default_and_not_add_it_if_key_not_exists()
{
Assert.Equal(0, valueDictionary.GetOrDefault(12));
Assert.False(valueDictionary.ContainsKey(12));
}
[Fact]
public void GetOrAddDefault_should_return_value_if_key_exists()
{

12
frontend/src/app/features/content/pages/content/content-page.component.html

@ -55,13 +55,12 @@
<sqx-notifo topic="apps/{{contentsState.appId}}/schemas/{{schema?.name}}/contents/{{content.id}}"></sqx-notifo>
<ng-container *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons"
(languageChange)="changeLanguage($event)"
[language]="language"
[languages]="languages">
[languages]="languages"
[percents]="contentForm.translationStatus | async">
</sqx-language-selector>
</ng-container>
<ng-container *ngIf="content?.canDelete">
<button type="button" class="btn btn-outline-secondary ms-2" (click)="dropdown.toggle()" #buttonOptions>
@ -97,6 +96,13 @@
</ng-container>
<ng-template #noContentMenu>
<sqx-language-selector class="languages-buttons"
(languageChange)="changeLanguage($event)"
[language]="language"
[languages]="languages"
[percents]="contentForm.translationStatus | async">
</sqx-language-selector>
<button type="button" class="btn btn-primary ms-2" (click)="save()" *ngIf="contentsState.canCreate | async">
{{ 'common.save' | sqxTranslate }}
</button>

4
frontend/src/app/features/content/pages/content/editor/content-field.component.html

@ -8,7 +8,7 @@
</button>
<sqx-field-languages
[field]="formModel.field"
[formModel]="formModel"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages"
@ -62,7 +62,7 @@
<div class="languages-container">
<div class="languages-buttons-compare">
<sqx-field-languages
[field]="formModelCompare!.field"
[formModel]="formModelCompare!"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages"

7
frontend/src/app/features/content/pages/content/editor/field-languages.component.html

@ -1,5 +1,5 @@
<ng-container *ngIf="field.isLocalizable && languages.length > 1">
<button *ngIf="!field.properties.isComplexUI" type="button" class="btn btn-text-secondary btn-sm me-1" (click)="toggleShowAllControls()">
<ng-container *ngIf="formModel.field.isLocalizable && languages.length > 1">
<button *ngIf="!formModel.field.properties.isComplexUI" type="button" class="btn btn-text-secondary btn-sm me-1" (click)="toggleShowAllControls()">
<ng-container *ngIf="showAllControls; else singleLanguage">
<span>{{ 'contents.languageModeSingle' | sqxTranslate }}</span>
</ng-container>
@ -9,9 +9,10 @@
</ng-template>
</button>
<ng-container *ngIf="field.properties.isComplexUI || !showAllControls">
<ng-container *ngIf="formModel.field.properties.isComplexUI || !showAllControls">
<div class="button-container">
<sqx-language-selector size="sm" #buttonLanguages
[exists]="formModel.translationStatus | async"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages">

6
frontend/src/app/features/content/pages/content/editor/field-languages.component.ts

@ -6,10 +6,10 @@
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { AppLanguageDto, RootFieldDto } from '@app/shared';
import { AppLanguageDto, FieldForm } from '@app/shared';
@Component({
selector: 'sqx-field-languages[field][language][languages]',
selector: 'sqx-field-languages[formModel][language][languages]',
styleUrls: ['./field-languages.component.scss'],
templateUrl: './field-languages.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -31,7 +31,7 @@ export class FieldLanguagesComponent {
public languages!: ReadonlyArray<AppLanguageDto>;
@Input()
public field!: RootFieldDto;
public formModel!: FieldForm;
public toggleShowAllControls() {
this.showAllControlsChange.emit(!this.showAllControls);

3
frontend/src/app/features/content/pages/contents/contents-page.component.html

@ -27,7 +27,8 @@
<sqx-language-selector class="languages-buttons"
(languageChange)="changeLanguage($event)"
[language]="language"
[languages]="languages">
[languages]="languages"
[percents]="translationStatus">
</sqx-language-selector>
</div>
<div class="col-auto">

8
frontend/src/app/features/content/pages/contents/contents-page.component.ts

@ -10,7 +10,7 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators';
import { AppLanguageDto, AppsState, ContentDto, ContentsState, ContributorsState, defined, LanguagesState, LocalStoreService, ModalModel, Queries, Query, QuerySynchronizer, ResourceOwner, Router2State, SchemaDto, SchemasService, SchemasState, Settings, switchSafe, TableSettings, TempService, UIState } from '@app/shared';
import { AppLanguageDto, AppsState, contentsTranslationStatus, ContentDto, ContentsState, ContributorsState, defined, LanguagesState, LocalStoreService, ModalModel, Queries, Query, QuerySynchronizer, ResourceOwner, Router2State, SchemaDto, SchemasService, SchemasState, Settings, switchSafe, TableSettings, TempService, TranslationStatus, UIState } from '@app/shared';
import { DueTimeSelectorComponent } from './../../shared/due-time-selector.component';
@Component({
@ -41,6 +41,8 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
public language!: AppLanguageDto;
public languages!: ReadonlyArray<AppLanguageDto>;
public translationStatus?: TranslationStatus;
public get disableScheduler() {
return this.appsState.snapshot.selectedSettings?.hideScheduler === true;
}
@ -115,8 +117,10 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.own(
this.contentsState.contents
.subscribe(() => {
.subscribe(contents => {
this.updateSelectionSummary();
this.translationStatus = contentsTranslationStatus(contents.map(x => x.data), this.schema, this.languages);
}));
}

8
frontend/src/app/features/content/shared/references/content-creator.component.html

@ -11,7 +11,13 @@
<div class="row gx-2 mt-3 mb-3">
<div class="col-auto">
<div *ngIf="schema && languages.length > 1">
<sqx-language-selector class="languages-buttons" [(language)]="language" [languages]="languages"></sqx-language-selector>
<sqx-language-selector class="languages-buttons"
dropdownPosition="bottom-left"
(languageChange)="language = $event"
[language]="language"
[languages]="languages"
[percents]="contentForm.translationStatus | async">
</sqx-language-selector>
</div>
</div>

2
frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts

@ -7,6 +7,7 @@
*/
import { Component } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { moduleMetadata } from '@storybook/angular';
import { Meta, Story } from '@storybook/angular/types-6-0';
import { LocalizerService, SqxFrameworkModule, TagEditorComponent } from '@app/framework';
@ -61,6 +62,7 @@ export default {
TestComponent,
],
imports: [
BrowserAnimationsModule,
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],

6
frontend/src/app/framework/angular/forms/forms-helper.ts

@ -104,11 +104,11 @@ export function valueProjection$<T = any>(form: AbstractControl, projection: (va
}
export function hasValue$(form: AbstractControl): Observable<boolean> {
return valueProjection$(form, v => isValid(v));
return valueProjection$(form, v => isValidValue(v));
}
export function hasNoValue$(form: AbstractControl): Observable<boolean> {
return valueProjection$(form, v => !isValid(v));
return valueProjection$(form, v => !isValidValue(v));
}
export function changed$(lhs: AbstractControl, rhs: AbstractControl) {
@ -155,7 +155,7 @@ export function touchedChange$(form: AbstractControl) {
});
}
function isValid(value: any) {
export function isValidValue(value: any) {
return !Types.isNull(value) && !Types.isUndefined(value);
}

34
frontend/src/app/framework/angular/language-selector.component.html

@ -1,24 +1,38 @@
<div class="btn-group btn-group-{{size}}" *ngIf="isSmallMode">
<button type="button" class="btn btn-outline-secondary" *ngFor="let supported of languages; trackBy: trackByLanguage" title="{{supported.englishName}}" [class.active]="supported === language" (click)="selectLanguage(supported)" tabindex="-1">
{{supported.iso2Code}}
</button>
</div>
<ng-container *ngIf="isLargeMode">
<ng-container *ngIf="languages.length > 1">
<ng-container *ngIf="languages.length > 3 || percents; else smallMode">
<button type="button" class="btn btn-outline-secondary btn-{{size}} dropdown-toggle" title="{{language.englishName}}" (click)="dropdown.toggle()" #button tabindex="-1">
{{language.iso2Code}}
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="button" [scrollY]="true">
<sqx-dropdown-menu [sqxAnchoredTo]="button" [scrollY]="true" [position]="dropdownPosition">
<table>
<tbody>
<tr class="dropdown-item" *ngFor="let supported of languages; trackBy: trackByLanguage" [class.active]="supported === language" (click)="selectLanguage(supported)">
<td><strong class="iso-code">{{supported.iso2Code}}</strong></td>
<tr class="dropdown-item" *ngFor="let supported of languages; trackBy: trackByLanguage"
[class.active]="supported === language"
[class.missing]="exists && !exists[supported.iso2Code]"
(click)="selectLanguage(supported)">
<td class="text-language">{{supported.iso2Code}}</td>
<td>({{supported.englishName}})</td>
<td *ngIf="percents" class="text-right">
{{percents[supported.iso2Code] || 0}} %
</td>
</tr>
</tbody>
</table>
</sqx-dropdown-menu>
</ng-container>
</ng-container>
<ng-template #smallMode>
<div class="btn-group btn-group-{{size}}">
<button type="button" class="btn btn-outline-secondary" *ngFor="let supported of languages; trackBy: trackByLanguage" title="{{supported.englishName}}"
[class.active]="supported === language"
[class.missing]="exists && !exists[supported.iso2Code]"
(click)="selectLanguage(supported)" tabindex="-1">
<span>{{supported.iso2Code}}</span>
</button>
</div>
</ng-template>
</ng-container>

21
frontend/src/app/framework/angular/language-selector.component.scss

@ -3,6 +3,7 @@
.dropdown-menu {
max-height: 20rem;
min-width: auto;
overflow-x: inherit;
overflow-y: auto;
}
@ -23,6 +24,22 @@ tr {
color: $color-text;
}
.iso-code {
font-family: monospace;
.missing {
&:not(.active) {
td {
opacity: .6;
}
span {
opacity: .6;
}
}
}
.text-language {
font-weight: bold;
}
.text-right {
text-align: right;
}

19
frontend/src/app/framework/angular/language-selector.component.ts

@ -6,7 +6,7 @@
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { ModalModel } from '@app/framework/internal';
import { ModalModel, RelativePosition } from '@app/framework/internal';
export interface Language { iso2Code: string; englishName: string; isMasterLanguage?: boolean }
@ -27,17 +27,18 @@ export class LanguageSelectorComponent implements OnChanges, OnInit {
public languages: ReadonlyArray<Language> = [];
@Input()
public size: 'sm' | 'md' | 'lg' = 'md';
public exists?: { [language: string]: boolean } | null;
public dropdown = new ModalModel();
@Input()
public percents?: { [language: string]: number } | null;
public get isSmallMode(): boolean {
return this.languages && this.languages.length > 0 && this.languages.length <= 3;
}
@Input()
public dropdownPosition: RelativePosition = 'bottom-right';
public get isLargeMode(): boolean {
return this.languages && this.languages.length > 3;
}
@Input()
public size: 'sm' | 'md' | 'lg' = 'md';
public dropdown = new ModalModel();
public ngOnChanges() {
this.update();

135
frontend/src/app/framework/angular/language-selector.stories.tsx

@ -0,0 +1,135 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { moduleMetadata } from '@storybook/angular';
import { Meta, Story } from '@storybook/angular/types-6-0';
import { LanguageSelectorComponent, SqxFrameworkModule } from '@app/framework';
export default {
title: 'Framework/Language-Selector',
component: LanguageSelectorComponent,
argTypes: {
size: {
control: 'enum',
options: [
'sm',
'md',
'lg',
],
},
},
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],
}),
],
} as Meta;
const Template: Story<LanguageSelectorComponent> = (args: LanguageSelectorComponent) => ({
props: args,
template: `
<sqx-root-view>
<div class="text-center">
<sqx-language-selector
[exists]="exists"
[language]="language"
[languages]="languages"
[percents]="percents">
</sqx-language-selector>
</div>
</sqx-root-view>
`,
});
export const Empty = Template.bind({});
Empty.args = {
languages: [],
};
export const OneLanguage = Template.bind({});
OneLanguage.args = {
languages: [
{ iso2Code: 'en', englishName: 'English' },
],
};
export const FewLanguages = Template.bind({});
FewLanguages.args = {
languages: [
{ iso2Code: 'en', englishName: 'English' },
{ iso2Code: 'it', englishName: 'Italian' },
{ iso2Code: 'es', englishName: 'Spanish' },
],
};
export const FewLanguagesWithExists = Template.bind({});
FewLanguagesWithExists.args = {
languages: [
{ iso2Code: 'en', englishName: 'English' },
{ iso2Code: 'it', englishName: 'Italian' },
{ iso2Code: 'es', englishName: 'Spanish' },
],
exists: {
en: true,
it: false,
es: true,
},
};
export const ManyLanguages = Template.bind({});
ManyLanguages.args = {
languages: [
{ iso2Code: 'en', englishName: 'English' },
{ iso2Code: 'it', englishName: 'Italian' },
{ iso2Code: 'es', englishName: 'Spanish' },
{ iso2Code: 'de', englishName: 'German' },
{ iso2Code: 'ru', englishName: 'Russian' },
],
};
export const ManyLanguagesWithExists = Template.bind({});
ManyLanguagesWithExists.args = {
languages: [
{ iso2Code: 'en', englishName: 'English' },
{ iso2Code: 'it', englishName: 'Italian' },
{ iso2Code: 'es', englishName: 'Spanish' },
{ iso2Code: 'de', englishName: 'German' },
{ iso2Code: 'ru', englishName: 'Russian' },
],
exists: {
en: true,
it: false,
es: true,
de: false,
ru: true,
},
};
export const WithPercents = Template.bind({});
WithPercents.args = {
languages: [
{ iso2Code: 'en', englishName: 'English' },
{ iso2Code: 'it', englishName: 'Italian' },
{ iso2Code: 'es', englishName: 'Spanish' },
],
percents: {
'en': 100,
'it': 67,
},
};

1
frontend/src/app/framework/utils/modal-positioner.ts

@ -13,6 +13,7 @@ export type AnchorX =
'left-to-left' |
'right-to-left' |
'right-to-right';
export type AnchorY =
'bottom-to-bottom' |
'bottom-to-top' |

61
frontend/src/app/shared/state/contents.forms-helpers.ts

@ -11,11 +11,72 @@
import { AbstractControl, ValidatorFn } from '@angular/forms';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { isValidValue, Language } from './../internal';
import { AppLanguageDto } from './../services/app-languages.service';
import { FieldDto, RootFieldDto, SchemaDto } from './../services/schemas.service';
import { fieldInvariant } from './../services/schemas.types';
import { CompiledRules, RuleContext, RulesProvider } from './contents.form-rules';
export type TranslationStatus = { [language: string]: number };
export function contentsTranslationStatus(datas: any[], schema: SchemaDto, languages: ReadonlyArray<Language>) {
const result: TranslationStatus = {};
for (const data of datas) {
const status = contentTranslationStatus(data, schema, languages);
for (const language of languages) {
const iso2Code = language.iso2Code;
result[iso2Code] = (result[iso2Code] || 0) + status[iso2Code];
}
}
for (const language of languages) {
const iso2Code = language.iso2Code;
result[iso2Code] = Math.round(result[iso2Code] / datas.length);
}
return result;
}
export function contentTranslationStatus(data: any, schema: SchemaDto, languages: ReadonlyArray<Language>) {
const result: TranslationStatus = {};
const localizedFields = schema.fields.filter(x => x.isLocalizable);
for (const language of languages) {
let percent = 0;
for (const field of localizedFields) {
if (isValidValue(data?.[field.name]?.[language.iso2Code])) {
percent++;
}
}
if (localizedFields.length > 0) {
percent = Math.round(100 * percent / localizedFields.length);
} else {
percent = 100;
}
result[language.iso2Code] = percent;
}
return result;
}
export function fieldTranslationStatus(data: any) {
const result: { [field: string]: boolean } = {};
for (const [key, value] of Object.entries(data)) {
result[key] = isValidValue(value);
}
return result;
}
export abstract class Hidden {
private readonly hidden$ = new BehaviorSubject<boolean>(false);

155
frontend/src/app/shared/state/contents.forms.spec.ts

@ -13,7 +13,7 @@ import { AppLanguageDto, createProperties, EditContentForm, getContentValue, Htm
import { FieldRule, SchemaDto } from './../services/schemas.service';
import { TestValues } from './_test-helpers';
import { ComponentForm, FieldArrayForm } from './contents.forms';
import { PartitionConfig } from './contents.forms-helpers';
import { contentsTranslationStatus, contentTranslationStatus, fieldTranslationStatus, PartitionConfig } from './contents.forms-helpers';
const {
createField,
@ -21,6 +21,159 @@ const {
createSchema,
} = TestValues;
describe('TranslationStatus', () => {
const languages = [
{ iso2Code: 'en' },
{ iso2Code: 'de' },
{ iso2Code: 'it' },
];
it('should create field status', () => {
const data = {
en: '',
de: 'field2',
it: true,
es: null,
};
const result = fieldTranslationStatus(data);
expect(result).toEqual({
en: true,
de: true,
it: true,
es: false,
});
});
it('should create content status for empty schema', () => {
const schema = {
fields: [],
} as any;
const result = contentTranslationStatus({}, schema, languages as any);
expect(result).toEqual({
en: 100,
de: 100,
it: 100,
});
});
it('should create content status for schema without localized field', () => {
const schema = {
fields: [{
isLocalizable: false,
}],
} as any;
const result = contentTranslationStatus({}, schema, languages as any);
expect(result).toEqual({
en: 100,
de: 100,
it: 100,
});
});
it('should create content status for schema with localized field', () => {
const schema = {
fields: [{
isLocalizable: true,
}],
} as any;
const result = contentTranslationStatus({}, schema, languages as any);
expect(result).toEqual({
en: 0,
de: 0,
it: 0,
});
});
it('should create content status for schema with mixed fields', () => {
const schema = {
fields: [{
name: 'field1', isLocalizable: true,
}, {
name: 'field2', isLocalizable: true,
}, {
name: 'field3', isLocalizable: true,
}, {
name: 'field4',
}],
} as any;
const data = {
field1: {
en: 'en',
de: 'de',
},
field2: {
en: 'en',
de: 'de',
},
field3: {
en: 'en',
},
};
const result = contentTranslationStatus(data, schema, languages as any);
expect(result).toEqual({
en: 100,
de: 67,
it: 0,
});
});
it('should create contents status', () => {
const schema = {
fields: [{
name: 'field1', isLocalizable: true,
}, {
name: 'field2', isLocalizable: true,
}, {
name: 'field3', isLocalizable: true,
}, {
name: 'field4',
}],
} as any;
const data1 = {
field1: {
en: 'en',
de: 'de',
},
field2: {
en: 'en',
de: 'de',
},
field3: {
en: 'en',
},
};
const data2 = {
field1: {
de: 'de',
},
field3: {
en: 'en',
},
};
const result = contentsTranslationStatus([data1, data2], schema, languages as any);
expect(result).toEqual({
en: 67,
de: 50,
it: 0,
});
});
});
describe('GetContentValue', () => {
const language = new LanguageDto('en', 'English');
const fieldInvariant = createField({ properties: createProperties('Number'), partitioning: 'invariant' });

10
frontend/src/app/shared/state/contents.forms.ts

@ -7,7 +7,7 @@
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { debounceTimeSafe, ExtendedFormGroup, Form, FormArrayTemplate, TemplatedFormArray, Types, value$ } from '@app/framework';
import { FormGroupTemplate, TemplatedFormGroup } from '@app/framework/angular/forms/templated-form-group';
import { AppLanguageDto } from './../services/app-languages.service';
@ -15,7 +15,7 @@ import { LanguageDto } from './../services/languages.service';
import { FieldDto, RootFieldDto, SchemaDto, TableField } from './../services/schemas.service';
import { ComponentFieldPropertiesDto, fieldInvariant } from './../services/schemas.types';
import { ComponentRulesProvider, RootRulesProvider, RulesProvider } from './contents.form-rules';
import { AbstractContentForm, AbstractContentFormState, FieldSection, FormGlobals, groupFields, PartitionConfig } from './contents.forms-helpers';
import { AbstractContentForm, AbstractContentFormState, contentTranslationStatus, FieldSection, fieldTranslationStatus, FormGlobals, groupFields, PartitionConfig } from './contents.forms-helpers';
import { FieldDefaultValue, FieldsValidators } from './contents.forms.visitors';
type SaveQueryFormType = { name: string; user: boolean };
@ -89,6 +89,9 @@ export class EditContentForm extends Form<ExtendedFormGroup, any> {
return this.valueChange$.value;
}
public readonly translationStatus =
this.valueChange$.pipe(map(x => contentTranslationStatus(x, this.schema, this.languages)));
constructor(
public readonly languages: ReadonlyArray<AppLanguageDto>,
public readonly schema: SchemaDto, schemas: { [id: string ]: SchemaDto },
@ -203,6 +206,9 @@ export class FieldForm extends AbstractContentForm<RootFieldDto, FormGroup> {
private readonly partitions: { [partition: string]: FieldItemForm } = {};
private isRequired: boolean;
public readonly translationStatus =
value$(this.form).pipe(map(x => fieldTranslationStatus(x)));
constructor(
globals: FormGlobals,
field: RootFieldDto,

Loading…
Cancel
Save