From 3ad0c839161ced29477b67ada08bbcd7f5576cba Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 20 Nov 2020 15:57:46 +0100 Subject: [PATCH] Feature/default values (#597) * Started with default values. * More tests and equality fix. * More tests. --- backend/i18n/frontend_en.json | 6 +- backend/i18n/frontend_it.json | 4 + backend/i18n/frontend_nl.json | 4 + backend/i18n/source/backend__ignore.json | 4 +- backend/i18n/source/frontend__ignore.json | 4 +- backend/i18n/source/frontend_en.json | 6 +- .../Schemas/AssetsFieldProperties.cs | 2 + .../Schemas/BooleanFieldProperties.cs | 2 + .../Schemas/DateTimeFieldProperties.cs | 6 +- .../Schemas/LocalizedValue.cs | 32 ++++ .../Schemas/NumberFieldProperties.cs | 6 +- .../Schemas/ReferencesFieldProperties.cs | 6 +- .../Schemas/StringFieldProperties.cs | 14 +- .../Schemas/TagsFieldProperties.cs | 6 +- .../DefaultValues/DefaultValueFactory.cs | 46 ++++-- .../DefaultValues/DefaultValueGenerator.cs | 2 +- .../CollectionExtensions.cs | 9 +- .../Reflection/SimpleEquals.cs | 6 +- .../Models/Fields/AssetsFieldPropertiesDto.cs | 5 + .../Fields/BooleanFieldPropertiesDto.cs | 5 + .../Fields/DateTimeFieldPropertiesDto.cs | 5 + .../Fields/GeolocationFieldPropertiesDto.cs | 5 - .../Models/Fields/NumberFieldPropertiesDto.cs | 5 + .../Fields/ReferencesFieldPropertiesDto.cs | 5 + .../Models/Fields/StringFieldPropertiesDto.cs | 5 + .../Models/Fields/TagsFieldPropertiesDto.cs | 5 + .../Model/Schemas/FieldCompareTests.cs | 59 +++++++ .../DefaultValues/DefaultValuesTests.cs | 146 ++++++++++++++++-- .../Reflection/SimpleEqualsTests.cs | 4 + .../pages/content/content-page.component.ts | 4 +- .../pages/contents/contents-page.component.ts | 4 +- .../schema/fields/field-wizard.component.html | 8 +- .../schema/fields/field-wizard.component.ts | 7 +- .../pages/schema/fields/field.component.html | 15 +- .../pages/schema/fields/field.component.ts | 9 +- .../forms/field-form-common.component.html | 8 +- .../fields/forms/field-form-ui.component.html | 4 +- .../field-form-validation.component.html | 75 +++++++-- .../forms/field-form-validation.component.ts | 8 +- .../fields/forms/field-form.component.html | 17 +- .../fields/forms/field-form.component.scss | 3 + .../fields/forms/field-form.component.ts | 8 +- .../fields/schema-fields.component.html | 33 ++-- .../schema/fields/schema-fields.component.ts | 5 +- .../types/array-validation.component.html | 19 ++- .../types/array-validation.component.scss | 14 +- .../fields/types/assets-ui.component.html | 2 +- .../types/assets-validation.component.html | 139 +++++++++++------ .../types/assets-validation.component.scss | 19 +-- .../types/assets-validation.component.ts | 11 +- .../fields/types/boolean-ui.component.html | 2 +- .../types/boolean-validation.component.html | 12 ++ .../types/boolean-validation.component.ts | 11 +- .../fields/types/date-time-ui.component.html | 2 +- .../types/date-time-validation.component.html | 12 ++ .../types/date-time-validation.component.ts | 15 +- .../fields/types/number-ui.component.html | 4 +- .../types/number-validation.component.html | 33 +++- .../types/number-validation.component.scss | 10 +- .../types/number-validation.component.ts | 11 +- .../references-validation.component.html | 35 +++-- .../references-validation.component.scss | 14 +- .../types/references-validation.component.ts | 11 +- .../fields/types/string-ui.component.html | 4 +- .../types/string-validation.component.html | 79 ++++++---- .../types/string-validation.component.scss | 10 +- .../types/string-validation.component.ts | 11 +- .../fields/types/tags-ui.component.html | 4 +- .../types/tags-validation.component.html | 33 +++- .../types/tags-validation.component.scss | 14 +- .../fields/types/tags-validation.component.ts | 11 +- .../editors/date-time-editor.component.ts | 11 +- .../editors/localized-input.component.html | 56 +++++++ .../editors/localized-input.component.scss | 0 .../editors/localized-input.component.ts | 100 ++++++++++++ .../forms/editors/tag-editor.component.ts | 11 +- .../angular}/language-selector.component.html | 0 .../angular}/language-selector.component.scss | 0 .../angular}/language-selector.component.ts | 12 +- frontend/app/framework/declarations.ts | 2 + frontend/app/framework/module.ts | 6 +- .../shared/components/notifo.component.scss | 36 +++-- frontend/app/shared/declarations.ts | 1 - frontend/app/shared/module.ts | 4 +- frontend/app/shared/services/schemas.types.ts | 9 ++ .../app/shared/state/contents.forms.spec.ts | 66 ++++++-- frontend/app/shared/state/contents.forms.ts | 29 ++-- .../shared/state/contents.forms.visitors.ts | 27 ++-- frontend/app/shared/state/languages.state.ts | 3 + 89 files changed, 1184 insertions(+), 353 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/LocalizedValue.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldCompareTests.cs create mode 100644 frontend/app/framework/angular/forms/editors/localized-input.component.html create mode 100644 frontend/app/framework/angular/forms/editors/localized-input.component.scss create mode 100644 frontend/app/framework/angular/forms/editors/localized-input.component.ts rename frontend/app/{shared/components/forms => framework/angular}/language-selector.component.html (100%) rename frontend/app/{shared/components/forms => framework/angular}/language-selector.component.scss (100%) rename frontend/app/{shared/components/forms => framework/angular}/language-selector.component.ts (82%) diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index d4f95ce37..721b55fdf 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -626,6 +626,7 @@ "rules.wizard.selectAction": "Select Action", "rules.wizard.selectTrigger": "Select Trigger", "rules.wizard.triggerHint": "The selection of the trigger type cannot be changed later.", + "schema.fields.localConfirmText": "Lock field", "schemas.addField": "Add Field", "schemas.addFieldAndClose": "Create and close", "schemas.addFieldAndCreate": "Create and add field", @@ -659,6 +660,8 @@ "schemas.export.synchronize": "Synchronize", "schemas.field.allowedValues": "Allowed Values", "schemas.field.defaultValue": "Default Value", + "schemas.field.defaultValues": "Default Values", + "schemas.field.defaultValuesHint": "Set the default value per language and override the default value property, if defined. Only use it if really needed.", "schemas.field.deleteConfirmText": "Do you really want to delete the field?", "schemas.field.deleteConfirmTitle": "Delete field", "schemas.field.disable": "Disable in UI", @@ -680,7 +683,8 @@ "schemas.field.localizableHint": "You can mark the field as localizable. It means that is dependent on the language, for example a city name.", "schemas.field.localizableMarker": "localizable", "schemas.field.lock": "Lock and prevent changes", - "schemas.field.lockConfirmText": "WARNING: Locking a field cannot be undone! Locked field definitions cannot be unlocked, deleted, or changed anymore. Do you really want to lock this field?", + "schemas.field.lockConfirmText": "WARNING: Locking a field cannot be undone! Locked field definitions cannot be unlocked, deleted, or changed anymore.\n\nDo you really want to lock this field?", + "schemas.field.lockConfirmTitle": "Lock field", "schemas.field.lockedMarker": "Locked", "schemas.field.nameHint": "The name of the field in the API response.", "schemas.field.namePlaceholder": "Enter field name", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 941047edb..36afbb102 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -626,6 +626,7 @@ "rules.wizard.selectAction": "Seleziona l'Azione", "rules.wizard.selectTrigger": "Seleziona l'Attivazione", "rules.wizard.triggerHint": "La selezione del tipo di attivazione non potrà essere modificata successivamente.", + "schema.fields.localConfirmText": "Lock field", "schemas.addField": "Aggiungi un Campo", "schemas.addFieldAndClose": "Crea e chiudi", "schemas.addFieldAndCreate": "Crea e aggiungi il campo", @@ -659,6 +660,8 @@ "schemas.export.synchronize": "Sincronizza", "schemas.field.allowedValues": "Valori consentiti", "schemas.field.defaultValue": "Valori predefiniti", + "schemas.field.defaultValues": "Default Values", + "schemas.field.defaultValuesHint": "Set the default value per language and override the default value property, if defined. Only use it if really needed.", "schemas.field.deleteConfirmText": "Sei sicuro di voler eliminare il campo?", "schemas.field.deleteConfirmTitle": "Cancella il campo", "schemas.field.disable": "Disabilita nella UI", @@ -681,6 +684,7 @@ "schemas.field.localizableMarker": "consente la localizzazione", "schemas.field.lock": "Blocca e impedisci i cambiamenti", "schemas.field.lockConfirmText": "Attenzione: Bloccare un campo è un'azione irreversibile! Se blocchi il campo non potrai più sbloccarlo o cancellarlo o cambiarlo. Sei sicuro di voler bloccare il campo?", + "schemas.field.lockConfirmTitle": "Lock field", "schemas.field.lockedMarker": "Bloccato", "schemas.field.nameHint": "Il nome del campo nelle chiamate API response.", "schemas.field.namePlaceholder": "Inserisci il nome del campo", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 13a11cd4c..72e4ceb2c 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -626,6 +626,7 @@ "rules.wizard.selectAction": "Selecteer actie", "rules.wizard.selectTrigger": "Selecteer Trigger", "rules.wizard.triggerHint": "De selectie van het triggertype kan later niet worden gewijzigd.", + "schema.fields.localConfirmText": "Lock field", "schemas.addField": "Veld toevoegen", "schemas.addFieldAndClose": "Maken en sluiten", "schemas.addFieldAndCreate": "Maak en voeg veld toe", @@ -659,6 +660,8 @@ "schemas.export.synchronize": "Synchroniseren", "schemas.field.allowedValues": "Toegestane waarden", "schemas.field.defaultValue": "Standaardwaarde", + "schemas.field.defaultValues": "Default Values", + "schemas.field.defaultValuesHint": "Set the default value per language and override the default value property, if defined. Only use it if really needed.", "schemas.field.deleteConfirmText": "Weet je zeker dat je het veld wilt verwijderen?", "schemas.field.deleteConfirmTitle": "Verwijder veld", "schemas.field.disable": "Uitschakelen in gebruikersinterface", @@ -681,6 +684,7 @@ "schemas.field.localizableMarker": "localizable", "schemas.field.lock": "Vergrendel en voorkom wijzigingen", "schemas.field.lockConfirmText": "WAARSCHUWING: het vergrendelen van een veld kan niet ongedaan worden gemaakt! Vergrendelde velddefinities kunnen niet meer worden ontgrendeld, verwijderd of gewijzigd. Wil je dit veld echt vergrendelen?", + "schemas.field.lockConfirmTitle": "Lock field", "schemas.field.lockedMarker": "Vergrendeld", "schemas.field.nameHint": "De naam van het veld in het API-antwoord.", "schemas.field.namePlaceholder": "Voer veldnaam in", diff --git a/backend/i18n/source/backend__ignore.json b/backend/i18n/source/backend__ignore.json index e9596e8ee..a678d4af6 100644 --- a/backend/i18n/source/backend__ignore.json +++ b/backend/i18n/source/backend__ignore.json @@ -151,10 +151,10 @@ "/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs": [ "*" ], - "/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/IndexManager.cs": [ + "/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/IndexManager_Impl.cs": [ "*" ], - "/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/IndexManager_Impl.cs": [ + "/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/IndexManager.cs": [ "*" ], "/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs": [ diff --git a/backend/i18n/source/frontend__ignore.json b/backend/i18n/source/frontend__ignore.json index 737bd389f..4635dc8c0 100644 --- a/backend/i18n/source/frontend__ignore.json +++ b/backend/i18n/source/frontend__ignore.json @@ -19,8 +19,8 @@ "#{{index + 1}}" ], "/features/content/shared/forms/field-editor.component.html": [ - "*", - "{{field.displayName}} {{displaySuffix}}" + "{{field.displayName}} {{displaySuffix}}", + "*" ], "/features/content/shared/references/references-editor.component.html": [ "·" diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index d4f95ce37..721b55fdf 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -626,6 +626,7 @@ "rules.wizard.selectAction": "Select Action", "rules.wizard.selectTrigger": "Select Trigger", "rules.wizard.triggerHint": "The selection of the trigger type cannot be changed later.", + "schema.fields.localConfirmText": "Lock field", "schemas.addField": "Add Field", "schemas.addFieldAndClose": "Create and close", "schemas.addFieldAndCreate": "Create and add field", @@ -659,6 +660,8 @@ "schemas.export.synchronize": "Synchronize", "schemas.field.allowedValues": "Allowed Values", "schemas.field.defaultValue": "Default Value", + "schemas.field.defaultValues": "Default Values", + "schemas.field.defaultValuesHint": "Set the default value per language and override the default value property, if defined. Only use it if really needed.", "schemas.field.deleteConfirmText": "Do you really want to delete the field?", "schemas.field.deleteConfirmTitle": "Delete field", "schemas.field.disable": "Disable in UI", @@ -680,7 +683,8 @@ "schemas.field.localizableHint": "You can mark the field as localizable. It means that is dependent on the language, for example a city name.", "schemas.field.localizableMarker": "localizable", "schemas.field.lock": "Lock and prevent changes", - "schemas.field.lockConfirmText": "WARNING: Locking a field cannot be undone! Locked field definitions cannot be unlocked, deleted, or changed anymore. Do you really want to lock this field?", + "schemas.field.lockConfirmText": "WARNING: Locking a field cannot be undone! Locked field definitions cannot be unlocked, deleted, or changed anymore.\n\nDo you really want to lock this field?", + "schemas.field.lockConfirmTitle": "Lock field", "schemas.field.lockedMarker": "Locked", "schemas.field.nameHint": "The name of the field in the API response.", "schemas.field.namePlaceholder": "Enter field name", diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs index 6ef5f8a10..d09e2ac39 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs @@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Core.Schemas { public AssetPreviewMode PreviewMode { get; set; } + public LocalizedValue DefaultValues { get; set; } + public string[]? DefaultValue { get; set; } public int? MinItems { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs index 24cc02b47..7820d3c64 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs @@ -10,6 +10,8 @@ namespace Squidex.Domain.Apps.Core.Schemas [Equals(DoNotAddEqualityOperators = true)] public sealed class BooleanFieldProperties : FieldProperties { + public LocalizedValue DefaultValues { get; set; } + public bool? DefaultValue { get; set; } public bool InlineEditable { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs index a65529f0a..0327165b4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs @@ -12,12 +12,14 @@ namespace Squidex.Domain.Apps.Core.Schemas [Equals(DoNotAddEqualityOperators = true)] public sealed class DateTimeFieldProperties : FieldProperties { + public LocalizedValue DefaultValues { get; set; } + + public Instant? DefaultValue { get; set; } + public Instant? MaxValue { get; set; } public Instant? MinValue { get; set; } - public Instant? DefaultValue { get; set; } - public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } public DateTimeFieldEditor Editor { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/LocalizedValue.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/LocalizedValue.cs new file mode 100644 index 000000000..b6a8eda44 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/LocalizedValue.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection.Equality; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class LocalizedValue : Dictionary, IEquatable> + { + public override bool Equals(object? obj) + { + return Equals(obj as LocalizedValue); + } + + public bool Equals(Dictionary? other) + { + return this.EqualsDictionary(other, EqualityComparer.Default, DeepEqualityComparer.Default); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(EqualityComparer.Default, DeepEqualityComparer.Default); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs index 9a85d5d1a..60dbb9930 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs @@ -14,12 +14,14 @@ namespace Squidex.Domain.Apps.Core.Schemas { public ReadOnlyCollection? AllowedValues { get; set; } + public LocalizedValue DefaultValues { get; set; } + + public double? DefaultValue { get; set; } + public double? MaxValue { get; set; } public double? MinValue { get; set; } - public double? DefaultValue { get; set; } - public bool IsUnique { get; set; } public bool InlineEditable { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs index 7f36badf0..1eb2d93a2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs @@ -15,6 +15,10 @@ namespace Squidex.Domain.Apps.Core.Schemas [Equals(DoNotAddEqualityOperators = true)] public sealed class ReferencesFieldProperties : FieldProperties { + public LocalizedValue DefaultValues { get; set; } + + public string[]? DefaultValue { get; set; } + public int? MinItems { get; set; } public int? MaxItems { get; set; } @@ -25,8 +29,6 @@ namespace Squidex.Domain.Apps.Core.Schemas public bool MustBePublished { get; set; } - public string[]? DefaultValue { get; set; } - public ReferencesFieldEditor Editor { get; set; } public DomainId SchemaId diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs index 47932d0c8..d783291a4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs @@ -14,6 +14,14 @@ namespace Squidex.Domain.Apps.Core.Schemas { public ReadOnlyCollection? AllowedValues { get; set; } + public LocalizedValue DefaultValues { get; set; } + + public string? DefaultValue { get; set; } + + public string? Pattern { get; set; } + + public string? PatternMessage { get; set; } + public int? MinLength { get; set; } public int? MaxLength { get; set; } @@ -30,12 +38,6 @@ namespace Squidex.Domain.Apps.Core.Schemas public bool InlineEditable { get; set; } - public string? DefaultValue { get; set; } - - public string? Pattern { get; set; } - - public string? PatternMessage { get; set; } - public StringContentType ContentType { get; set; } public StringFieldEditor Editor { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs index 772ae01b6..1f5c287fb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs @@ -14,12 +14,14 @@ namespace Squidex.Domain.Apps.Core.Schemas { public ReadOnlyCollection? AllowedValues { get; set; } + public LocalizedValue DefaultValues { get; set; } + + public string[]? DefaultValue { get; set; } + public int? MinItems { get; set; } public int? MaxItems { get; set; } - public string[]? DefaultValue { get; set; } - public TagsFieldEditor Editor { get; set; } public TagsFieldNormalization Normalization { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs index c8910c7b5..971008610 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs @@ -16,17 +16,19 @@ namespace Squidex.Domain.Apps.Core.DefaultValues public sealed class DefaultValueFactory : IFieldVisitor { private readonly Instant now; + private readonly string partition; - private DefaultValueFactory(Instant now) + private DefaultValueFactory(Instant now, string partition) { this.now = now; + this.partition = partition; } - public static IJsonValue CreateDefaultValue(IField field, Instant now) + public static IJsonValue CreateDefaultValue(IField field, Instant now, string partition) { Guard.NotNull(field, nameof(field)); - return field.Accept(new DefaultValueFactory(now)); + return field.Accept(new DefaultValueFactory(now, partition)); } public IJsonValue Visit(IArrayField field) @@ -36,12 +38,16 @@ namespace Squidex.Domain.Apps.Core.DefaultValues public IJsonValue Visit(IField field) { - return Array(field.Properties.DefaultValue); + var value = GetDefaultValue(field.Properties.DefaultValue, field.Properties.DefaultValues); + + return Array(value); } public IJsonValue Visit(IField field) { - return JsonValue.Create(field.Properties.DefaultValue); + var value = GetDefaultValue(field.Properties.DefaultValue, field.Properties.DefaultValues); + + return JsonValue.Create(value); } public IJsonValue Visit(IField field) @@ -56,22 +62,30 @@ namespace Squidex.Domain.Apps.Core.DefaultValues public IJsonValue Visit(IField field) { - return JsonValue.Create(field.Properties.DefaultValue); + var value = GetDefaultValue(field.Properties.DefaultValue, field.Properties.DefaultValues); + + return JsonValue.Create(value); } public IJsonValue Visit(IField field) { - return Array(field.Properties.DefaultValue); + var value = GetDefaultValue(field.Properties.DefaultValue, field.Properties.DefaultValues); + + return Array(value); } public IJsonValue Visit(IField field) { - return JsonValue.Create(field.Properties.DefaultValue); + var value = GetDefaultValue(field.Properties.DefaultValue, field.Properties.DefaultValues); + + return JsonValue.Create(value); } public IJsonValue Visit(IField field) { - return Array(field.Properties.DefaultValue); + var value = GetDefaultValue(field.Properties.DefaultValue, field.Properties.DefaultValues); + + return Array(value); } public IJsonValue Visit(IField field) @@ -91,7 +105,19 @@ namespace Squidex.Domain.Apps.Core.DefaultValues return JsonValue.Create($"{now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}T00:00:00Z"); } - return JsonValue.Create(field.Properties.DefaultValue); + var value = GetDefaultValue(field.Properties.DefaultValue, field.Properties.DefaultValues); + + return JsonValue.Create(value); + } + + private T GetDefaultValue(T value, LocalizedValue? values) + { + if (values != null && values.TryGetValue(partition, out var @default)) + { + return @default; + } + + return value; } private static IJsonValue Array(string[]? values) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueGenerator.cs index 11c66c239..da162f013 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueGenerator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueGenerator.cs @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Core.DefaultValues { Guard.NotNull(fieldData, nameof(fieldData)); - var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant()); + var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant(), partitionKey); if (field.RawProperties.IsRequired || defaultValue == null || defaultValue.Type == JsonValueType.Null) { diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs index ddee81e66..049814061 100644 --- a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -170,18 +170,23 @@ namespace Squidex.Infrastructure return hashCode; } - public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other) where TKey : notnull + public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary? other) where TKey : notnull { return EqualsDictionary(dictionary, other, EqualityComparer.Default, EqualityComparer.Default); } - public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary other, IEqualityComparer keyComparer, IEqualityComparer valueComparer) where TKey : notnull + public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IReadOnlyDictionary? other, IEqualityComparer keyComparer, IEqualityComparer valueComparer) where TKey : notnull { if (other == null) { return false; } + if (ReferenceEquals(dictionary, other)) + { + return true; + } + if (dictionary.Count != other.Count) { return false; diff --git a/backend/src/Squidex.Infrastructure/Reflection/SimpleEquals.cs b/backend/src/Squidex.Infrastructure/Reflection/SimpleEquals.cs index b04cf7708..75a741789 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/SimpleEquals.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/SimpleEquals.cs @@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.Reflection { public static class SimpleEquals { - private static readonly ConcurrentDictionary Comparers = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary Comparers = new ConcurrentDictionary(); private static readonly HashSet SimpleTypes = new HashSet(); private static readonly DefaultComparer DefaultComparer = new DefaultComparer(); private static readonly NoopComparer NoopComparer = new NoopComparer(); @@ -38,13 +38,13 @@ namespace Squidex.Infrastructure.Reflection return BuildCore(type) ?? NoopComparer; } - private static IDeepComparer BuildCore(Type t) + private static IDeepComparer? BuildCore(Type t) { return Comparers.GetOrAdd(t, type => { if (IsSimpleType(type) || IsEquatable(type)) { - return NoopComparer; + return null; } if (IsArray(type)) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs index eec8c2b30..e52592d1f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs @@ -19,6 +19,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public AssetPreviewMode PreviewMode { get; set; } + /// + /// The language specific default value as a list of asset ids. + /// + public LocalizedValue DefaultValues { get; set; } + /// /// The default value as a list of asset ids. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs index 1e3e8652f..620d709cf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs @@ -12,6 +12,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { public sealed class BooleanFieldPropertiesDto : FieldPropertiesDto { + /// + /// The language specific default value for the field value. + /// + public LocalizedValue DefaultValues { get; set; } + /// /// The default value for the field value. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs index e776a0ecf..a529168a1 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs @@ -13,6 +13,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { public sealed class DateTimeFieldPropertiesDto : FieldPropertiesDto { + /// + /// The language specific default value for the field value. + /// + public LocalizedValue DefaultValues { get; set; } + /// /// The default value for the field value. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs index 004d6c740..572d52378 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs @@ -12,11 +12,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { public sealed class GeolocationFieldPropertiesDto : FieldPropertiesDto { - /// - /// The default value for the field value. - /// - public bool? DefaultValue { get; set; } - /// /// The editor that is used to manage this field. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs index 4108d68d0..c27f3393d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs @@ -13,6 +13,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { public sealed class NumberFieldPropertiesDto : FieldPropertiesDto { + /// + /// The language specific default value for the field value. + /// + public LocalizedValue DefaultValues { get; set; } + /// /// The default value for the field value. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs index ff547b651..60605b3b3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs @@ -14,6 +14,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { public sealed class ReferencesFieldPropertiesDto : FieldPropertiesDto { + /// + /// The language specific default value as a list of content ids. + /// + public LocalizedValue DefaultValues { get; set; } + /// /// The default value as a list of content ids. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs index efdc6c6bf..486a6464d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs @@ -13,6 +13,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { public sealed class StringFieldPropertiesDto : FieldPropertiesDto { + /// + /// The language specific default value for the field value. + /// + public LocalizedValue DefaultValues { get; set; } + /// /// The default value for the field value. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs index 70ca5d043..47b210069 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs @@ -13,6 +13,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { public sealed class TagsFieldPropertiesDto : FieldPropertiesDto { + /// + /// The language specific default value for the field value. + /// + public LocalizedValue DefaultValues { get; set; } + /// /// The default value. /// diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldCompareTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldCompareTests.cs new file mode 100644 index 000000000..05d4b95fb --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldCompareTests.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Schemas +{ + public class FieldCompareTests + { + [Fact] + public void Should_compare_two_string_fields_as_equal() + { + var lhs = new StringFieldProperties + { + DefaultValues = new LocalizedValue + { + ["iv"] = "ABC" + } + }; + + var rhs = new StringFieldProperties + { + DefaultValues = new LocalizedValue + { + ["iv"] = "ABC" + } + }; + + Assert.Equal(lhs, rhs); + } + + [Fact] + public void Should_compare_two_tags_fields_as_equal() + { + var lhs = new TagsFieldProperties + { + DefaultValues = new LocalizedValue + { + ["iv"] = new string[] { "A", "B", "C" } + } + }; + + var rhs = new TagsFieldProperties + { + DefaultValues = new LocalizedValue + { + ["iv"] = new string[] { "A", "B", "C" } + } + }; + + Assert.Equal(lhs, rhs); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs index ec64e8e72..6783bdb2c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs @@ -20,6 +20,7 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues { private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10); private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE); + private readonly Language language = Language.DE; private readonly Schema schema; public DefaultValuesTests() @@ -85,7 +86,7 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.Assets(1, "1", Partitioning.Invariant, new AssetsFieldProperties()); - Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -95,7 +96,24 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.Assets(1, "1", Partitioning.Invariant, new AssetsFieldProperties { DefaultValue = new[] { "1", "2" } }); - Assert.Equal(JsonValue.Array("1", "2"), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Array("1", "2"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_assets_field_when_localized() + { + var field = + Fields.Assets(1, "1", Partitioning.Invariant, + new AssetsFieldProperties + { + DefaultValues = new LocalizedValue + { + [language.Iso2Code] = null + }, + DefaultValue = new[] { "1", "2" } + }); + + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -105,7 +123,24 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.Boolean(1, "1", Partitioning.Invariant, new BooleanFieldProperties { DefaultValue = true }); - Assert.Equal(JsonValue.True, DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.True, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_boolean_field_when_localized() + { + var field = + Fields.Boolean(1, "1", Partitioning.Invariant, + new BooleanFieldProperties + { + DefaultValues = new LocalizedValue + { + [language.Iso2Code] = null + }, + DefaultValue = true + }); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -115,7 +150,7 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.DateTime(1, "1", Partitioning.Invariant, new DateTimeFieldProperties { DefaultValue = FutureDays(15) }); - Assert.Equal(JsonValue.Create(FutureDays(15)), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Create(FutureDays(15)), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -125,7 +160,7 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.DateTime(1, "1", Partitioning.Invariant, new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }); - Assert.Equal(JsonValue.Create("2017-10-12T00:00:00Z"), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Create("2017-10-12T00:00:00Z"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -135,7 +170,24 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.DateTime(1, "1", Partitioning.Invariant, new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }); - Assert.Equal(JsonValue.Create("2017-10-12T16:30:10Z"), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Create("2017-10-12T16:30:10Z"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_when_localized() + { + var field = + Fields.DateTime(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties + { + DefaultValues = new LocalizedValue + { + [language.Iso2Code] = null + }, + DefaultValue = FutureDays(15) + }); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -145,7 +197,7 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.Json(1, "1", Partitioning.Invariant, new JsonFieldProperties()); - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -155,7 +207,7 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.Geolocation(1, "1", Partitioning.Invariant, new GeolocationFieldProperties()); - Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -165,7 +217,24 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.Number(1, "1", Partitioning.Invariant, new NumberFieldProperties { DefaultValue = 12 }); - Assert.Equal(JsonValue.Create(12), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Create(12), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_number_field_when_localized() + { + var field = + Fields.Number(1, "1", Partitioning.Invariant, + new NumberFieldProperties + { + DefaultValues = new LocalizedValue + { + [language.Iso2Code] = null + }, + DefaultValue = 12 + }); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -175,7 +244,7 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.References(1, "1", Partitioning.Invariant, new ReferencesFieldProperties()); - Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -185,7 +254,24 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.References(1, "1", Partitioning.Invariant, new ReferencesFieldProperties { DefaultValue = new[] { "1", "2" } }); - Assert.Equal(JsonValue.Array("1", "2"), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Array("1", "2"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_references_field_when_localized() + { + var field = + Fields.References(1, "1", Partitioning.Invariant, + new ReferencesFieldProperties + { + DefaultValues = new LocalizedValue + { + [language.Iso2Code] = null + }, + DefaultValue = new[] { "1", "2" } + }); + + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -195,7 +281,24 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.String(1, "1", Partitioning.Invariant, new StringFieldProperties { DefaultValue = "default" }); - Assert.Equal(JsonValue.Create("default"), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Create("default"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_string_field_when_localized() + { + var field = + Fields.String(1, "1", Partitioning.Invariant, + new StringFieldProperties + { + DefaultValues = new LocalizedValue + { + [language.Iso2Code] = null + }, + DefaultValue = "default" + }); + + Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } [Fact] @@ -205,7 +308,24 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues Fields.Tags(1, "1", Partitioning.Invariant, new TagsFieldProperties { DefaultValue = new[] { "tag1", "tag2" } }); - Assert.Equal(JsonValue.Array("tag1", "tag2"), DefaultValueFactory.CreateDefaultValue(field, now)); + Assert.Equal(JsonValue.Array("tag1", "tag2"), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); + } + + [Fact] + public void Should_get_default_value_from_tags_field_when_localized() + { + var field = + Fields.Tags(1, "1", Partitioning.Invariant, + new TagsFieldProperties + { + DefaultValues = new LocalizedValue + { + [language.Iso2Code] = null + }, + DefaultValue = new[] { "tag1", "tag2" } + }); + + Assert.Equal(JsonValue.Array(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code)); } private Instant FutureDays(int days) diff --git a/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleEqualsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleEqualsTests.cs index 9e358fa4a..2c1f29d8f 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleEqualsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleEqualsTests.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Reflection.Equality; using Xunit; #pragma warning disable CA1822 // Mark members as static @@ -98,7 +99,10 @@ namespace Squidex.Infrastructure.Reflection public void Should_compare_values(object lhs, object rhs) { Assert.True(SimpleEquals.IsEquals(lhs, lhs)); + Assert.True(DeepEqualityComparer.Default.Equals(lhs, lhs)); + Assert.False(SimpleEquals.IsEquals(lhs, rhs)); + Assert.True(DeepEqualityComparer.Default.Equals(lhs, rhs)); } [Fact] diff --git a/frontend/app/features/content/pages/content/content-page.component.ts b/frontend/app/features/content/pages/content/content-page.component.ts index e03b603ba..ede263e82 100644 --- a/frontend/app/features/content/pages/content/content-page.component.ts +++ b/frontend/app/features/content/pages/content/content-page.component.ts @@ -63,9 +63,9 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD this.contentsState.loadIfNotLoaded(); this.own( - this.languagesState.languages + this.languagesState.languagesDtos .subscribe(languages => { - this.languages = languages.map(x => x.language); + this.languages = languages; this.language = this.languages.find(x => x.isMaster)!; })); diff --git a/frontend/app/features/content/pages/contents/contents-page.component.ts b/frontend/app/features/content/pages/contents/contents-page.component.ts index ee362526d..49a293096 100644 --- a/frontend/app/features/content/pages/contents/contents-page.component.ts +++ b/frontend/app/features/content/pages/contents/contents-page.component.ts @@ -94,9 +94,9 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit { })); this.own( - this.languagesState.languages + this.languagesState.languagesDtos .subscribe(languages => { - this.languages = languages.map(x => x.language); + this.languages = languages; this.language = this.languages.find(x => x.isMaster)!; this.languageMaster = this.language; })); diff --git a/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html b/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html index 2142e7441..9d5e04e4a 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html @@ -63,7 +63,13 @@
- +
diff --git a/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.ts b/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.ts index f5a525696..4fa424248 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.ts @@ -7,7 +7,7 @@ import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { FormBuilder } from '@angular/forms'; -import { AddFieldForm, createProperties, EditFieldForm, FieldDto, fieldTypes, PatternsState, RootFieldDto, SchemaDetailsDto, SchemasState, Types } from '@app/shared'; +import { AddFieldForm, createProperties, EditFieldForm, FieldDto, fieldTypes, LanguagesState, PatternsState, RootFieldDto, SchemaDetailsDto, SchemasState, Types } from '@app/shared'; const DEFAULT_FIELD = { name: '', partitioning: 'invariant', properties: createProperties('String') }; @@ -29,6 +29,10 @@ export class FieldWizardComponent implements OnInit { @Output() public complete = new EventEmitter(); + public get isLocalizable() { + return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable']; + } + public fieldTypes = fieldTypes; public field: FieldDto; @@ -40,6 +44,7 @@ export class FieldWizardComponent implements OnInit { constructor( private readonly formBuilder: FormBuilder, private readonly schemasState: SchemasState, + public readonly languagesState: LanguagesState, public readonly patternsState: PatternsState ) {} diff --git a/frontend/app/features/schemas/pages/schema/fields/field.component.html b/frontend/app/features/schemas/pages/schema/fields/field.component.html index bf5b3db73..5f2f7e71f 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/field.component.html @@ -67,7 +67,7 @@ {{ 'schemas.field.lock' | sqxTranslate }} @@ -94,7 +94,13 @@
- +
@@ -112,7 +118,7 @@ - + @@ -127,7 +133,8 @@ - + diff --git a/frontend/app/features/schemas/pages/schema/fields/field.component.ts b/frontend/app/features/schemas/pages/schema/fields/field.component.ts index 0a804b260..55ca8cf9a 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/field.component.ts @@ -8,7 +8,7 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { FormBuilder } from '@angular/forms'; -import { createProperties, DialogModel, EditFieldForm, fadeAnimation, ModalModel, NestedFieldDto, PatternDto, RootFieldDto, SchemaDetailsDto, SchemasState, sorted } from '@app/shared'; +import { createProperties, DialogModel, EditFieldForm, fadeAnimation, LanguageDto, ModalModel, NestedFieldDto, PatternDto, RootFieldDto, SchemaDetailsDto, SchemasState, sorted } from '@app/shared'; @Component({ selector: 'sqx-field', @@ -28,9 +28,16 @@ export class FieldComponent implements OnChanges { @Input() public parent: RootFieldDto; + @Input() + public languages: ReadonlyArray; + @Input() public patterns: ReadonlyArray; + public get isLocalizable() { + return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable']; + } + public dropdown = new ModalModel(); public trackByFieldFn: (_index: number, field: NestedFieldDto) => any; diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html index 7627cc088..c2e4cb78f 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html @@ -2,7 +2,7 @@
-
+
@@ -14,7 +14,7 @@
-
+
@@ -28,7 +28,7 @@
-
+
@@ -42,7 +42,7 @@
-
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html index e66cf7103..7cbbfca1a 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html @@ -2,7 +2,7 @@
-
+
@@ -44,7 +44,7 @@
-
+
- + +
- + +
- + +
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.scss b/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.scss index e69de29bb..326bbbf98 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.scss +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.scss @@ -0,0 +1,3 @@ +.table-items-row-details-tab { + padding-right: 3rem; +} \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.ts b/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.ts index 16ab3e60a..47adfdd74 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.ts @@ -7,7 +7,7 @@ import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { FieldDto, PatternDto } from '@app/shared'; +import { FieldDto, LanguageDto, PatternDto } from '@app/shared'; @Component({ selector: 'sqx-field-form', @@ -30,6 +30,12 @@ export class FieldFormComponent implements AfterViewInit { @Input() public patterns: ReadonlyArray; + @Input() + public languages: ReadonlyArray; + + @Input() + public isLocalizable: boolean; + @Output() public cancel = new EventEmitter(); diff --git a/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.html b/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.html index 1ae12c639..e28c0f1fe 100644 --- a/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.html @@ -6,25 +6,28 @@
- -
-
- - - + + +
+
+ + + +
-
- + + - + \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.ts b/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.ts index 7cd45c635..1111411d7 100644 --- a/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.ts @@ -7,7 +7,7 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { Component, Input, OnInit } from '@angular/core'; -import { DialogModel, FieldDto, fieldTypes, PatternsState, SchemaDetailsDto, SchemasState, sorted } from '@app/shared'; +import { DialogModel, FieldDto, fieldTypes, LanguagesState, PatternsState, SchemaDetailsDto, SchemasState, sorted } from '@app/shared'; @Component({ selector: 'sqx-schema-fields', @@ -26,6 +26,7 @@ export class SchemaFieldsComponent implements OnInit { constructor( public readonly schemasState: SchemasState, + public readonly languageState: LanguagesState, public readonly patternsState: PatternsState ) { this.trackByFieldFn = this.trackByField.bind(this); @@ -33,6 +34,8 @@ export class SchemaFieldsComponent implements OnInit { public ngOnInit() { this.patternsState.load(); + + this.languageState.load(); } public sortFields(event: CdkDragDrop>) { diff --git a/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html index 313523543..e9c85a88f 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html @@ -2,13 +2,18 @@
-
- - - -
-
- +
+
+
+ +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.scss b/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.scss index 63166b94e..85f6907b7 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.scss +++ b/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.scss @@ -1,13 +1,5 @@ .minmax { - &-col { - position: relative; - } - - &-label { - @include absolute(0, -.2rem, auto, auto); - } -} - -.form-group { - margin-top: .5rem; + text-align: center; + text-decoration: none; + width: 2rem; } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html index 87f84510e..b460bcadd 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html @@ -3,7 +3,7 @@
-
+
- - -
-
- +
+
+
+ +
+
+ +
+
+ +
+
+
+
-
- - - -
-
- -
-
- +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
@@ -42,48 +54,63 @@
-
- - - -
-
- -
-
- +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
-
- - - -
-
- -
-
- +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
-
- - - -
-
- -
-
- +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
@@ -98,12 +125,12 @@
-
+
-
+
@@ -111,8 +138,20 @@
-
+
+ +
+ + +
+ + + + {{ 'schemas.field.defaultValuesHint' | sqxTranslate }} + +
+
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.scss b/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.scss index 347cb10dc..0682072a0 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.scss +++ b/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.scss @@ -1,17 +1,10 @@ .minmax { - &-col { - position: relative; - } - - &-label { - @include absolute(0, -.2rem, auto, auto); - } -} - -.form-group { - margin-top: .5rem; + text-align: center; + text-decoration: none; + width: 2rem; } -.form-group2 { - margin-top: 3rem; +.col-label { + @include force-width(5rem); + padding-left: .5rem; } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts index 7067f6b7b..58b3ec8a8 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { AssetsFieldPropertiesDto, FieldDto } from '@app/shared'; +import { AssetsFieldPropertiesDto, FieldDto, LanguageDto } from '@app/shared'; @Component({ selector: 'sqx-assets-validation', @@ -24,6 +24,12 @@ export class AssetsValidationComponent implements OnInit { @Input() public properties: AssetsFieldPropertiesDto; + @Input() + public languages: ReadonlyArray; + + @Input() + public isLocalizable: boolean; + public ngOnInit() { this.fieldForm.setControl('minItems', new FormControl(this.properties.minItems)); @@ -66,5 +72,8 @@ export class AssetsValidationComponent implements OnInit { this.fieldForm.setControl('defaultValue', new FormControl(this.properties.defaultValue)); + + this.fieldForm.setControl('defaultValues', + new FormControl(this.properties.defaultValues)); } } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html index 6ab153481..59b9ad97e 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html @@ -2,7 +2,7 @@
-
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/boolean-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/boolean-validation.component.html index dc554a3eb..176be3c63 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/boolean-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/boolean-validation.component.html @@ -9,4 +9,16 @@
+ +
+ + +
+ + + + {{ 'schemas.field.defaultValuesHint' | sqxTranslate }} + +
+
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts index 235c572a4..763d32545 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { BooleanFieldPropertiesDto, FieldDto, hasNoValue$ } from '@app/shared'; +import { BooleanFieldPropertiesDto, FieldDto, hasNoValue$, LanguageDto } from '@app/shared'; import { Observable } from 'rxjs'; @Component({ @@ -25,12 +25,21 @@ export class BooleanValidationComponent implements OnInit { @Input() public properties: BooleanFieldPropertiesDto; + @Input() + public languages: ReadonlyArray; + + @Input() + public isLocalizable: boolean; + public showDefaultValue: Observable; public ngOnInit() { this.fieldForm.setControl('defaultValue', new FormControl(this.properties.defaultValue)); + this.fieldForm.setControl('defaultValues', + new FormControl(this.properties.defaultValues)); + this.fieldForm.setControl('inlineEditable', new FormControl(this.properties.inlineEditable)); diff --git a/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html index ceb950b89..d403ed259 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html @@ -2,7 +2,7 @@
-
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/date-time-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/date-time-validation.component.html index f25e74224..6bdc12f1e 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/date-time-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/date-time-validation.component.html @@ -34,5 +34,17 @@
+ +
+ + +
+ + + + {{ 'schemas.field.defaultValuesHint' | sqxTranslate }} + +
+
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts index f14ef5bad..fa9ea9d8c 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { DateTimeFieldPropertiesDto, FieldDto, hasNoValue$, ValidatorsEx } from '@app/shared'; +import { DateTimeFieldPropertiesDto, FieldDto, hasNoValue$, LanguageDto, ValidatorsEx } from '@app/shared'; import { Observable } from 'rxjs'; @Component({ @@ -25,6 +25,12 @@ export class DateTimeValidationComponent implements OnInit { @Input() public properties: DateTimeFieldPropertiesDto; + @Input() + public languages: ReadonlyArray; + + @Input() + public isLocalizable: boolean; + public showDefaultValues: Observable; public showDefaultValue: Observable; @@ -45,9 +51,10 @@ export class DateTimeValidationComponent implements OnInit { ])); this.fieldForm.setControl('defaultValue', - new FormControl(this.properties.defaultValue, [ - ValidatorsEx.validDateTime() - ])); + new FormControl(this.properties.defaultValue)); + + this.fieldForm.setControl('defaultValues', + new FormControl(this.properties.defaultValues)); this.showDefaultValues = hasNoValue$(this.fieldForm.controls['isRequired']); diff --git a/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html index a80b8b1a3..eeb7e5e31 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html @@ -2,7 +2,7 @@
-
+
@@ -26,7 +26,7 @@
-
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.html index 2c10cf17e..47694e81e 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.html @@ -13,21 +13,38 @@
-
- - - -
-
- +
+
+
+ +
+
+ +
+
+ +
+
-
+
+ +
+ + +
+ + + + {{ 'schemas.field.defaultValuesHint' | sqxTranslate }} + +
+
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.scss b/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.scss index c7b39858f..85f6907b7 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.scss +++ b/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.scss @@ -1,9 +1,5 @@ .minmax { - &-col { - position: relative; - } - - &-label { - @include absolute(0, -2rem, auto, auto); - } + text-align: center; + text-decoration: none; + width: 2rem; } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.ts index b32cf0e83..5887babe7 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { FieldDto, NumberFieldPropertiesDto, RootFieldDto, Types } from '@app/shared'; +import { FieldDto, LanguageDto, NumberFieldPropertiesDto, RootFieldDto, Types } from '@app/shared'; @Component({ selector: 'sqx-number-validation', @@ -24,6 +24,12 @@ export class NumberValidationComponent implements OnInit { @Input() public properties: NumberFieldPropertiesDto; + @Input() + public languages: ReadonlyArray; + + @Input() + public isLocalizable: boolean; + public showUnique: boolean; public ngOnInit() { @@ -42,5 +48,8 @@ export class NumberValidationComponent implements OnInit { this.fieldForm.setControl('defaultValue', new FormControl(this.properties.defaultValue)); + + this.fieldForm.setControl('defaultValues', + new FormControl(this.properties.defaultValues)); } } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.html index 23d938f16..6771a4f94 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.html @@ -2,7 +2,7 @@
-
+
@@ -23,13 +23,18 @@
-
- - - -
-
- +
+
+
+ +
+
+ +
+
+ +
+
@@ -47,9 +52,21 @@
-
+
+ +
+ + +
+ + + + {{ 'schemas.field.defaultValuesHint' | sqxTranslate }} + +
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.scss b/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.scss index 63166b94e..85f6907b7 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.scss +++ b/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.scss @@ -1,13 +1,5 @@ .minmax { - &-col { - position: relative; - } - - &-label { - @include absolute(0, -.2rem, auto, auto); - } -} - -.form-group { - margin-top: .5rem; + text-align: center; + text-decoration: none; + width: 2rem; } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.ts index f68d13e59..03cfa4141 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { FieldDto, ReferencesFieldPropertiesDto, SchemaTagSource } from '@app/shared'; +import { FieldDto, LanguageDto, ReferencesFieldPropertiesDto, SchemaTagSource } from '@app/shared'; @Component({ selector: 'sqx-references-validation', @@ -24,6 +24,12 @@ export class ReferencesValidationComponent implements OnInit { @Input() public properties: ReferencesFieldPropertiesDto; + @Input() + public languages: ReadonlyArray; + + @Input() + public isLocalizable: boolean; + constructor( public readonly schemasSource: SchemaTagSource ) { @@ -45,6 +51,9 @@ export class ReferencesValidationComponent implements OnInit { this.fieldForm.setControl('defaultValue', new FormControl(this.properties.defaultValue)); + this.fieldForm.setControl('defaultValues', + new FormControl(this.properties.defaultValues)); + this.fieldForm.setControl('mustBePublished', new FormControl(this.properties.mustBePublished)); } diff --git a/frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.html index f8501110d..13a92b3b7 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.html @@ -2,7 +2,7 @@
-
+
@@ -89,7 +89,7 @@
-
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html index de0281e42..48df65eb2 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html @@ -13,20 +13,25 @@
-
- - - -
-
- +
+
+
+ +
+
+ +
+
+ +
+
-
+
@@ -49,13 +54,13 @@
-
+
-
+
{{ 'schemas.fieldTypes.string.wordHint' | sqxTranslate}} @@ -64,7 +69,7 @@
-
+
@@ -73,33 +78,55 @@
-
- - - -
-
- +
+
+
+ +
+
+ +
+
+ +
+
-
- - - -
-
- +
+
+
+ +
+
+ +
+
+ +
+
-
+
+ +
+ + +
+ + + + {{ 'schemas.field.defaultValuesHint' | sqxTranslate }} + +
+
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.scss b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.scss index 922cb129e..330739795 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.scss +++ b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.scss @@ -1,11 +1,7 @@ .minmax { - &-col { - position: relative; - } - - &-label { - @include absolute(0, -.2rem, auto, auto); - } + text-align: center; + text-decoration: none; + width: 2rem; } .control-dropdown { diff --git a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts index eb3c1d744..0149db2c9 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { fadeAnimation, FieldDto, hasNoValue$, hasValue$, ModalModel, PatternDto, ResourceOwner, RootFieldDto, StringFieldPropertiesDto, STRING_CONTENT_TYPES, Types, value$ } from '@app/shared'; +import { fadeAnimation, FieldDto, hasNoValue$, hasValue$, LanguageDto, ModalModel, PatternDto, ResourceOwner, RootFieldDto, StringFieldPropertiesDto, STRING_CONTENT_TYPES, Types, value$ } from '@app/shared'; import { Observable } from 'rxjs'; @Component({ @@ -31,6 +31,12 @@ export class StringValidationComponent extends ResourceOwner implements OnChange @Input() public patterns: ReadonlyArray; + @Input() + public languages: ReadonlyArray; + + @Input() + public isLocalizable: boolean; + public contentTypes = STRING_CONTENT_TYPES; public showPatternMessage: Observable; @@ -79,6 +85,9 @@ export class StringValidationComponent extends ResourceOwner implements OnChange this.fieldForm.setControl('defaultValue', new FormControl(this.properties.defaultValue)); + this.fieldForm.setControl('defaultValues', + new FormControl(this.properties.defaultValues)); + this.showPatternSuggestions = hasNoValue$(this.fieldForm.controls['pattern']); diff --git a/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.html index 8930335c6..95b99f1fa 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.html @@ -2,7 +2,7 @@
-
+
@@ -26,7 +26,7 @@
-
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.html index 571e6303c..47ad95a59 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.html @@ -2,21 +2,38 @@
-
- - - -
-
- +
+
+
+ +
+
+ +
+
+ +
+
-
+
+ +
+ + +
+ + + + {{ 'schemas.field.defaultValuesHint' | sqxTranslate }} + +
+
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.scss b/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.scss index 63166b94e..85f6907b7 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.scss +++ b/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.scss @@ -1,13 +1,5 @@ .minmax { - &-col { - position: relative; - } - - &-label { - @include absolute(0, -.2rem, auto, auto); - } -} - -.form-group { - margin-top: .5rem; + text-align: center; + text-decoration: none; + width: 2rem; } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.ts index 0ba07f3d2..e61178725 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/tags-validation.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { FieldDto, TagsFieldPropertiesDto } from '@app/shared'; +import { FieldDto, LanguageDto, TagsFieldPropertiesDto } from '@app/shared'; @Component({ selector: 'sqx-tags-validation', @@ -24,6 +24,12 @@ export class TagsValidationComponent implements OnInit { @Input() public properties: TagsFieldPropertiesDto; + @Input() + public languages: ReadonlyArray; + + @Input() + public isLocalizable: boolean; + public ngOnInit() { this.fieldForm.setControl('maxItems', new FormControl(this.properties.maxItems)); @@ -33,5 +39,8 @@ export class TagsValidationComponent implements OnInit { this.fieldForm.setControl('defaultValue', new FormControl(this.properties.defaultValue)); + + this.fieldForm.setControl('defaultValues', + new FormControl(this.properties.defaultValues)); } } \ No newline at end of file diff --git a/frontend/app/framework/angular/forms/editors/date-time-editor.component.ts b/frontend/app/framework/angular/forms/editors/date-time-editor.component.ts index ed2fd406d..14cc2caf9 100644 --- a/frontend/app/framework/angular/forms/editors/date-time-editor.component.ts +++ b/frontend/app/framework/angular/forms/editors/date-time-editor.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { DateHelper, DateTime, StatefulControlComponent, UIOptions } from '@app/framework/internal'; import * as Pikaday from 'pikaday/pikaday'; @@ -35,6 +35,9 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string private dateTime: DateTime | null; private suppressEvents = false; + @Output() + public blur = new EventEmitter(); + @Input() public mode: 'DateTime' | 'Date'; @@ -110,6 +113,12 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string this.updateControls(); } + public callTouched() { + this.blur.next(); + + super.callTouched(); + } + public setDisabledState(isDisabled: boolean): void { super.setDisabledState(isDisabled); diff --git a/frontend/app/framework/angular/forms/editors/localized-input.component.html b/frontend/app/framework/angular/forms/editors/localized-input.component.html new file mode 100644 index 000000000..be9d70a9e --- /dev/null +++ b/frontend/app/framework/angular/forms/editors/localized-input.component.html @@ -0,0 +1,56 @@ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
\ No newline at end of file diff --git a/frontend/app/framework/angular/forms/editors/localized-input.component.scss b/frontend/app/framework/angular/forms/editors/localized-input.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/framework/angular/forms/editors/localized-input.component.ts b/frontend/app/framework/angular/forms/editors/localized-input.component.ts new file mode 100644 index 000000000..74e7fb72f --- /dev/null +++ b/frontend/app/framework/angular/forms/editors/localized-input.component.ts @@ -0,0 +1,100 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + + // tslint:disable: whitespace + +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { fadeAnimation, ModalModel, StatefulControlComponent, Types } from '@app/framework/internal'; +import { Language } from './../../language-selector.component'; + +export const SQX_LOCALIZED_INPUT_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => LocalizedInputComponent), multi: true +}; + +const DEFAULT_LANGUAGE = { iso2Code: 'iv' }; + +@Component({ + selector: 'sqx-localized-input', + styleUrls: ['./localized-input.component.scss'], + templateUrl: './localized-input.component.html', + providers: [ + SQX_LOCALIZED_INPUT_CONTROL_VALUE_ACCESSOR + ], + animations: [ + fadeAnimation + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class LocalizedInputComponent extends StatefulControlComponent<{}, { [key: string]: any }> { + private value: { [key: string]: any } | undefined; + + @Input() + public languages: ReadonlyArray; + + @Input() + public type: 'text' | 'boolean' | 'datetime' | 'date' | 'tags' = 'text'; + + @Input() + public name: string; + + @Input() + public id: string; + + public selectedLanguage: Language; + + public dropdown = new ModalModel(); + + public get currentValue() { + if (!this.selectedLanguage || !this.value) { + return undefined; + } + + return this.value[this.selectedLanguage.iso2Code]; + } + + public get isEmpty() { + if (!this.selectedLanguage || !this.value) { + return true; + } + + return !this.value.hasOwnProperty(this.selectedLanguage.iso2Code); + } + + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + selectedLanguage: DEFAULT_LANGUAGE + }); + } + + public writeValue(obj: any) { + if (Types.isObject(obj)) { + this.value = obj; + } else { + this.value = {}; + } + } + + public setValue(value: any) { + this.value = { ...this.value || {} }; + this.value[this.selectedLanguage.iso2Code] = value; + + this.callChange(this.value); + } + + public unset() { + this.value= { ...this.value || {} }; + + delete this.value[this.selectedLanguage.iso2Code]; + + if (Object.keys(this.value).length === 0) { + this.value = undefined; + } + + this.callChange(this.value); + } +} \ No newline at end of file diff --git a/frontend/app/framework/angular/forms/editors/tag-editor.component.ts b/frontend/app/framework/angular/forms/editors/tag-editor.component.ts index e7fc176c4..39f6ad1dc 100644 --- a/frontend/app/framework/angular/forms/editors/tag-editor.component.ts +++ b/frontend/app/framework/angular/forms/editors/tag-editor.component.ts @@ -7,7 +7,7 @@ // tslint:disable: template-use-track-by-function -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { fadeAnimation, getTagValues, Keys, ModalModel, StatefulControlComponent, StringConverter, TagValue, Types } from '@app/framework/internal'; import { distinctUntilChanged, map, tap } from 'rxjs/operators'; @@ -57,6 +57,9 @@ export class TagEditorComponent extends StatefulControlComponent; + @Output() + public blur = new EventEmitter(); + @Input() public converter = StringConverter.INSTANCE; @@ -377,6 +380,12 @@ export class TagEditorComponent extends StatefulControlComponent x.id === tagValue.id); } + public callTouched() { + this.blur.next(); + + super.callTouched(); + } + public onCut(event: ClipboardEvent) { if (!this.hasSelection()) { this.onCopy(event); diff --git a/frontend/app/shared/components/forms/language-selector.component.html b/frontend/app/framework/angular/language-selector.component.html similarity index 100% rename from frontend/app/shared/components/forms/language-selector.component.html rename to frontend/app/framework/angular/language-selector.component.html diff --git a/frontend/app/shared/components/forms/language-selector.component.scss b/frontend/app/framework/angular/language-selector.component.scss similarity index 100% rename from frontend/app/shared/components/forms/language-selector.component.scss rename to frontend/app/framework/angular/language-selector.component.scss diff --git a/frontend/app/shared/components/forms/language-selector.component.ts b/frontend/app/framework/angular/language-selector.component.ts similarity index 82% rename from frontend/app/shared/components/forms/language-selector.component.ts rename to frontend/app/framework/angular/language-selector.component.ts index 50c20c32a..edcc4c63c 100644 --- a/frontend/app/shared/components/forms/language-selector.component.ts +++ b/frontend/app/framework/angular/language-selector.component.ts @@ -6,7 +6,7 @@ */ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; -import { fadeAnimation, LanguageDto, ModalModel } from '@app/shared/internal'; +import { fadeAnimation, ModalModel } from '@app/framework/internal'; export interface Language { iso2Code: string; englishName: string; isMasterLanguage: true; } @@ -24,7 +24,7 @@ export class LanguageSelectorComponent implements OnChanges, OnInit { public selectedLanguageChange = new EventEmitter(); @Input() - public size: string; + public size: 'sm' | 'md' | 'lg' = 'md'; @Input() public languages: ReadonlyArray = []; @@ -61,11 +61,13 @@ export class LanguageSelectorComponent implements OnChanges, OnInit { } public selectLanguage(language: Language) { - this.selectedLanguage = language; - this.selectedLanguageChange.emit(language); + if (language?.iso2Code !== this.selectedLanguage?.iso2Code) { + this.selectedLanguage = language; + this.selectedLanguageChange.emit(language); + } } - public trackByLanguage(_index: number, language: LanguageDto) { + public trackByLanguage(_index: number, language: Language) { return language.iso2Code; } } \ No newline at end of file diff --git a/frontend/app/framework/declarations.ts b/frontend/app/framework/declarations.ts index c9647f2a6..16121a039 100644 --- a/frontend/app/framework/declarations.ts +++ b/frontend/app/framework/declarations.ts @@ -20,6 +20,7 @@ export * from './angular/forms/editors/date-time-editor.component'; export * from './angular/forms/editors/dropdown.component'; export * from './angular/forms/editors/iframe-editor.component'; export * from './angular/forms/editors/json-editor.component'; +export * from './angular/forms/editors/localized-input.component'; export * from './angular/forms/editors/stars.component'; export * from './angular/forms/editors/tag-editor.component'; export * from './angular/forms/editors/toggle.component'; @@ -40,6 +41,7 @@ export * from './angular/http/caching.interceptor'; export * from './angular/http/http-extensions'; export * from './angular/http/loading.interceptor'; export * from './angular/image-source.directive'; +export * from './angular/language-selector.component'; export * from './angular/list-view.component'; export * from './angular/modals/dialog-renderer.component'; export * from './angular/modals/modal-dialog.component'; diff --git a/frontend/app/framework/module.ts b/frontend/app/framework/module.ts index c79355aed..2c93a12a1 100644 --- a/frontend/app/framework/module.ts +++ b/frontend/app/framework/module.ts @@ -12,7 +12,7 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { ModuleWithProviders, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ColorPickerModule } from 'ngx-color-picker'; -import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, IFrameEditorComponent, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, JsonEditorComponent, KeysPipe, KNumberPipe, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalStoreService, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MoneyPipe, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, PanelComponent, PanelContainerDirective, ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, TooltipDirective, TransformInputDirective, TranslatePipe } from './declarations'; +import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, IFrameEditorComponent, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, JsonEditorComponent, KeysPipe, KNumberPipe, LanguageSelectorComponent, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MoneyPipe, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, PanelComponent, PanelContainerDirective, ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, TooltipDirective, TransformInputDirective, TranslatePipe } from './declarations'; @NgModule({ imports: [ @@ -59,8 +59,10 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc JsonEditorComponent, KeysPipe, KNumberPipe, + LanguageSelectorComponent, LightenPipe, ListViewComponent, + LocalizedInputComponent, MarkdownInlinePipe, MarkdownPipe, ModalDialogComponent, @@ -137,8 +139,10 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc JsonEditorComponent, KeysPipe, KNumberPipe, + LanguageSelectorComponent, LightenPipe, ListViewComponent, + LocalizedInputComponent, MarkdownInlinePipe, MarkdownPipe, ModalDialogComponent, diff --git a/frontend/app/shared/components/notifo.component.scss b/frontend/app/shared/components/notifo.component.scss index 74ffef7dc..8307f4c18 100644 --- a/frontend/app/shared/components/notifo.component.scss +++ b/frontend/app/shared/components/notifo.component.scss @@ -1,26 +1,28 @@ :host ::ng-deep { - .notifo-notifications-button { - margin-left: .75rem; - margin-right: .75rem; - margin-top: .25rem; + .notifo { + .notifo-notifications-button { + margin-left: .75rem; + margin-right: .75rem; + margin-top: .25rem; - svg { - fill: $color-white; + svg { + fill: $color-white; + } } - } - .notifo-topics-button { - margin-left: .75rem; - margin-right: .75rem; + .notifo-topics-button { + margin-left: .75rem; + margin-right: .75rem; - svg { - fill: darken($color-border, 30%); + svg { + fill: darken($color-border, 30%); + } } - } - .notifo-container { - display: inline-block; - max-width: 5rem; - min-width: 3rem; + .notifo-container { + display: inline-block; + max-width: 5rem; + min-width: 3rem; + } } } \ No newline at end of file diff --git a/frontend/app/shared/declarations.ts b/frontend/app/shared/declarations.ts index f85d9a3be..9d1e0bf76 100644 --- a/frontend/app/shared/declarations.ts +++ b/frontend/app/shared/declarations.ts @@ -21,7 +21,6 @@ export * from './components/assets/pipes'; export * from './components/comments/comment.component'; export * from './components/comments/comments.component'; export * from './components/forms/geolocation-editor.component'; -export * from './components/forms/language-selector.component'; export * from './components/forms/markdown-editor.component'; export * from './components/forms/references-checkboxes.component'; export * from './components/forms/references-dropdown.component'; diff --git a/frontend/app/shared/module.ts b/frontend/app/shared/module.ts index 39bc44ab1..ae52d4269 100644 --- a/frontend/app/shared/module.ts +++ b/frontend/app/shared/module.ts @@ -13,7 +13,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { SqxFrameworkModule } from '@app/framework'; import { MentionModule } from 'angular-mentions'; -import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetsDialogState, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetUploaderComponent, AssetUploaderState, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentMustExistGuard, ContentsService, ContentsState, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, GeolocationEditorComponent, GraphQlService, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, ImageCropperComponent, ImageFocusPointComponent, LanguageSelectorComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PatternsService, PatternsState, PlansService, PlansState, QueryComponent, QueryListComponent, QueryPathComponent, ReferencesCheckboxesComponent, ReferencesDropdownComponent, ReferencesTagsComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SortingComponent, StockPhotoService, TableHeaderComponent, TranslationsService, UIService, UIState, UnsetAppGuard, UnsetContentGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WorkflowsService, WorkflowsState } from './declarations'; +import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetsDialogState, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetUploaderComponent, AssetUploaderState, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentMustExistGuard, ContentsService, ContentsState, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, GeolocationEditorComponent, GraphQlService, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PatternsService, PatternsState, PlansService, PlansState, QueryComponent, QueryListComponent, QueryPathComponent, ReferencesCheckboxesComponent, ReferencesDropdownComponent, ReferencesTagsComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SortingComponent, StockPhotoService, TableHeaderComponent, TranslationsService, UIService, UIState, UnsetAppGuard, UnsetContentGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WorkflowsService, WorkflowsState } from './declarations'; import { SearchService } from './services/search.service'; @NgModule({ @@ -50,7 +50,6 @@ import { SearchService } from './services/search.service'; HistoryMessagePipe, ImageCropperComponent, ImageFocusPointComponent, - LanguageSelectorComponent, MarkdownEditorComponent, NotifoComponent, QueryComponent, @@ -94,7 +93,6 @@ import { SearchService } from './services/search.service'; HistoryComponent, HistoryListComponent, HistoryMessagePipe, - LanguageSelectorComponent, MarkdownEditorComponent, NotifoComponent, QueryListComponent, diff --git a/frontend/app/shared/services/schemas.types.ts b/frontend/app/shared/services/schemas.types.ts index cd749d86d..61761acd1 100644 --- a/frontend/app/shared/services/schemas.types.ts +++ b/frontend/app/shared/services/schemas.types.ts @@ -129,6 +129,8 @@ export interface FieldPropertiesVisitor { visitUI(properties: UIFieldPropertiesDto): T; } +type DefaultValue = { [key: string]: T | null | undefined }; + export abstract class FieldPropertiesDto { public abstract fieldType: FieldType; @@ -180,6 +182,7 @@ export class AssetsFieldPropertiesDto extends FieldPropertiesDto { public readonly previewMode: AssetPreviewMode; public readonly defaultValue?: ReadonlyArray; + public readonly defaultValues?: DefaultValue>; public readonly allowDuplicates?: boolean; public readonly allowedExtensions?: ReadonlyArray; public readonly resolveFirst: boolean; @@ -215,6 +218,7 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Boolean'; public readonly defaultValue?: boolean; + public readonly defaultValues?: DefaultValue; public readonly editor: BooleanFieldEditor = 'Checkbox'; public readonly inlineEditable: boolean = false; @@ -239,6 +243,7 @@ export class DateTimeFieldPropertiesDto extends FieldPropertiesDto { public readonly calculatedDefaultValue?: string; public readonly defaultValue?: string; + public readonly defaultValues?: DefaultValue; public readonly editor: DateTimeFieldEditor = 'DateTime'; public readonly maxValue?: string; public readonly minValue?: string; @@ -298,6 +303,7 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto { public readonly allowedValues?: ReadonlyArray; public readonly defaultValue?: number; + public readonly defaultValues?: DefaultValue; public readonly editor: NumberFieldEditor = 'Input'; public readonly inlineEditable: boolean = false; public readonly isUnique: boolean = false; @@ -327,6 +333,7 @@ export class ReferencesFieldPropertiesDto extends FieldPropertiesDto { public readonly allowDuplicates?: boolean; public readonly defaultValue?: ReadonlyArray; + public readonly defaultValues?: DefaultValue>; public readonly editor: ReferencesFieldEditor = 'List'; public readonly maxItems?: number; public readonly minItems?: number; @@ -374,6 +381,7 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto { public readonly allowedValues?: ReadonlyArray; public readonly defaultValue?: string; + public readonly defaultValues?: DefaultValue; public readonly editor: StringFieldEditor = 'Input'; public readonly inlineEditable: boolean = false; public readonly isUnique: boolean = false; @@ -409,6 +417,7 @@ export class TagsFieldPropertiesDto extends FieldPropertiesDto { public readonly allowedValues?: ReadonlyArray; public readonly defaultValue?: ReadonlyArray; + public readonly defaultValues?: DefaultValue>; public readonly editor: TagsFieldEditor = 'Tags'; public readonly maxItems?: number; public readonly minItems?: number; diff --git a/frontend/app/shared/state/contents.forms.spec.ts b/frontend/app/shared/state/contents.forms.spec.ts index b4622267d..c0f995a65 100644 --- a/frontend/app/shared/state/contents.forms.spec.ts +++ b/frontend/app/shared/state/contents.forms.spec.ts @@ -152,7 +152,7 @@ describe('ArrayField', () => { }); it('should return default value as null', () => { - expect(FieldDefaultValue.get(field)).toBeNull(); + expect(FieldDefaultValue.get(field, 'iv')).toBeNull(); }); }); @@ -182,7 +182,13 @@ describe('AssetsField', () => { it('should return default value from properties', () => { const field2 = createField({ properties: createProperties('Assets', { defaultValue: ['1', '2'] }) }); - expect(FieldDefaultValue.get(field2)).toEqual(['1', '2']); + expect(FieldDefaultValue.get(field2, 'iv')).toEqual(['1', '2']); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('Assets', { defaultValue: ['1', '2'], defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); }); }); @@ -208,7 +214,13 @@ describe('TagsField', () => { it('should return default value from properties', () => { const field2 = createField({ properties: createProperties('Tags', { defaultValue: ['1', '2'] }) }); - expect(FieldDefaultValue.get(field2)).toEqual(['1', '2']); + expect(FieldDefaultValue.get(field2, 'iv')).toEqual(['1', '2']); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('Tags', { defaultValue: ['1', '2'], defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); }); }); @@ -234,7 +246,13 @@ describe('BooleanField', () => { it('should return default value from properties', () => { const field2 = createField({ properties: createProperties('Boolean', { editor: 'Checkbox', defaultValue: true }) }); - expect(FieldDefaultValue.get(field2)).toBeTruthy(); + expect(FieldDefaultValue.get(field2, 'iv')).toBeTruthy(); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('Boolean', { defaultValue: true, defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); }); }); @@ -284,19 +302,25 @@ describe('DateTimeField', () => { it('should return default from properties value', () => { const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', defaultValue: '2017-10-12T16:00:00Z' }) }); - expect(FieldDefaultValue.get(field2)).toEqual('2017-10-12T16:00:00Z'); + expect(FieldDefaultValue.get(field2, 'iv')).toEqual('2017-10-12T16:00:00Z'); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('DateTime', { defaultValue: '2017-10-12T16:00:00Z', defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); }); it('should return default from Today', () => { const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', calculatedDefaultValue: 'Today' }) }); - expect(FieldDefaultValue.get(field2, now)).toEqual('2017-10-12T00:00:00Z'); + expect(FieldDefaultValue.get(field2, 'iv', now)).toEqual('2017-10-12T00:00:00Z'); }); it('should return default value from Today', () => { const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', calculatedDefaultValue: 'Now' }) }); - expect(FieldDefaultValue.get(field2, now)).toEqual('2017-10-12T16:30:10Z'); + expect(FieldDefaultValue.get(field2, 'iv', now)).toEqual('2017-10-12T16:30:10Z'); }); }); @@ -316,7 +340,7 @@ describe('GeolocationField', () => { }); it('should return default value as null', () => { - expect(FieldDefaultValue.get(field)).toBeNull(); + expect(FieldDefaultValue.get(field, 'iv')).toBeNull(); }); }); @@ -336,7 +360,7 @@ describe('JsonField', () => { }); it('should return default value as null', () => { - expect(FieldDefaultValue.get(field)).toBeNull(); + expect(FieldDefaultValue.get(field, 'iv')).toBeNull(); }); }); @@ -388,7 +412,13 @@ describe('NumberField', () => { it('should return default value from properties', () => { const field2 = createField({ properties: createProperties('Number', { defaultValue: 13 }) }); - expect(FieldDefaultValue.get(field2)).toEqual(13); + expect(FieldDefaultValue.get(field2, 'iv')).toEqual(13); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('Number', { defaultValue: 13, defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); }); }); @@ -418,7 +448,13 @@ describe('ReferencesField', () => { it('should return default value from properties', () => { const field2 = createField({ properties: createProperties('References', { defaultValue: ['1', '2'] }) }); - expect(FieldDefaultValue.get(field2)).toEqual(['1', '2']); + expect(FieldDefaultValue.get(field2, 'iv')).toEqual(['1', '2']); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('References', { defaultValue: ['1', '2'], defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); }); }); @@ -458,7 +494,13 @@ describe('StringField', () => { it('should return default value from properties', () => { const field2 = createField({ properties: createProperties('String', { defaultValue: 'MyDefault' }) }); - expect(FieldDefaultValue.get(field2)).toEqual('MyDefault'); + expect(FieldDefaultValue.get(field2, 'iv')).toEqual('MyDefault'); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('String', { defaultValue: 'MyDefault', defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); }); }); diff --git a/frontend/app/shared/state/contents.forms.ts b/frontend/app/shared/state/contents.forms.ts index 76f753ae1..9f58bf89f 100644 --- a/frontend/app/shared/state/contents.forms.ts +++ b/frontend/app/shared/state/contents.forms.ts @@ -219,8 +219,8 @@ export class FieldForm extends AbstractContentForm { for (const { key, isOptional } of partitions.getAll(field)) { const child = field.isArray ? - new FieldArrayForm(field, isOptional, rules, this.remoteValidator) : - new FieldValueForm(field, isOptional, this.remoteValidator); + new FieldArrayForm(field, isOptional, key, rules, this.remoteValidator) : + new FieldValueForm(field, isOptional, key, this.remoteValidator); this.partitions[key] = child; @@ -295,14 +295,14 @@ export class FieldForm extends AbstractContentForm { } export class FieldValueForm extends AbstractContentForm { - constructor(field: RootFieldDto, isOptional: boolean, + constructor(field: RootFieldDto, isOptional: boolean, key: string, remoteValidator?: ValidatorFn ) { - super(field, FieldValueForm.buildControl(field, isOptional, remoteValidator), isOptional); + super(field, FieldValueForm.buildControl(field, isOptional, key, remoteValidator), isOptional); } - private static buildControl(field: RootFieldDto, isOptional: boolean, remoteValidator?: ValidatorFn) { - const value = FieldDefaultValue.get(field); + private static buildControl(field: RootFieldDto, isOptional: boolean, key: string, remoteValidator?: ValidatorFn) { + const value = FieldDefaultValue.get(field, key); const validators = FieldsValidators.create(field, isOptional); @@ -330,6 +330,7 @@ export class FieldArrayForm extends AbstractContentForm } constructor(field: RootFieldDto, isOptional: boolean, + private readonly partition: string, private readonly allRules: CompiledRule[], private readonly remoteValidator?: ValidatorFn ) { @@ -341,7 +342,7 @@ export class FieldArrayForm extends AbstractContentForm } public addItem(source?: FieldArrayItemForm) { - const child = new FieldArrayItemForm(this.field, this.isOptional, this.allRules, source, this.remoteValidator); + const child = new FieldArrayItemForm(this.field, this.isOptional, this.allRules, source, this.partition, this.remoteValidator); this.items = [...this.items, child]; @@ -401,7 +402,7 @@ export class FieldArrayItemForm extends AbstractContentForm>; - constructor(field: RootFieldDto, isOptional: boolean, allRules: CompiledRule[], source: FieldArrayItemForm | undefined, + constructor(field: RootFieldDto, isOptional: boolean, allRules: CompiledRule[], source: FieldArrayItemForm | undefined, partition: string, private readonly remoteValidator?: ValidatorFn ) { super(field, new FormGroup({}), isOptional); @@ -413,7 +414,7 @@ export class FieldArrayItemForm extends AbstractContentForm { private isRequired = false; - constructor(field: NestedFieldDto, parent: RootFieldDto, rules: CompiledRule[], isOptional: boolean, source: FieldArrayItemForm | undefined, - remoteValidator?: ValidatorFn + constructor(field: NestedFieldDto, parent: RootFieldDto, rules: CompiledRule[], isOptional: boolean, partition: string, + source: FieldArrayItemForm | undefined, remoteValidator?: ValidatorFn ) { super(field, - FieldArrayItemValueForm.buildControl(field, isOptional, remoteValidator, source), + FieldArrayItemValueForm.buildControl(field, isOptional, partition, remoteValidator, source), isOptional, FieldArrayItemValueForm.buildRules(field, parent, rules) ); @@ -492,8 +493,8 @@ export class FieldArrayItemValueForm extends AbstractContentForm x.field === fullName); } - private static buildControl(field: NestedFieldDto, isOptional: boolean, remoteValidator?: ValidatorFn, source?: FieldArrayItemForm) { - let value = FieldDefaultValue.get(field); + private static buildControl(field: NestedFieldDto, isOptional: boolean, partition: string, remoteValidator?: ValidatorFn, source?: FieldArrayItemForm) { + let value = FieldDefaultValue.get(field, partition); if (source) { const sourceField = source.form.get(field.name); diff --git a/frontend/app/shared/state/contents.forms.visitors.ts b/frontend/app/shared/state/contents.forms.visitors.ts index 5bc102f7d..75e974c23 100644 --- a/frontend/app/shared/state/contents.forms.visitors.ts +++ b/frontend/app/shared/state/contents.forms.visitors.ts @@ -344,12 +344,13 @@ export class FieldsValidators implements FieldPropertiesVisitor { private constructor( + private readonly partitionKey: string, private readonly now?: DateTime ) { } - public static get(field: FieldDto, now?: DateTime) { - return field.properties.accept(new FieldDefaultValue(now)); + public static get(field: FieldDto, partitionKey: string, now?: DateTime) { + return field.properties.accept(new FieldDefaultValue(partitionKey, now)); } public visitDateTime(properties: DateTimeFieldPropertiesDto): any { @@ -360,7 +361,7 @@ export class FieldDefaultValue implements FieldPropertiesVisitor { } else if (properties.calculatedDefaultValue === 'Today') { return `${now.toISODate()}T00:00:00Z`; } else { - return properties.defaultValue; + return this.getValue(properties.defaultValue, properties.defaultValues); } } @@ -369,11 +370,11 @@ export class FieldDefaultValue implements FieldPropertiesVisitor { } public visitAssets(properties: AssetsFieldPropertiesDto): any { - return properties.defaultValue; + return this.getValue(properties.defaultValue, properties.defaultValues); } public visitBoolean(properties: BooleanFieldPropertiesDto): any { - return properties.defaultValue; + return this.getValue(properties.defaultValue, properties.defaultValues); } public visitGeolocation(_: GeolocationFieldPropertiesDto): any { @@ -385,22 +386,30 @@ export class FieldDefaultValue implements FieldPropertiesVisitor { } public visitNumber(properties: NumberFieldPropertiesDto): any { - return properties.defaultValue; + return this.getValue(properties.defaultValue, properties.defaultValues); } public visitReferences(properties: ReferencesFieldPropertiesDto): any { - return properties.defaultValue; + return this.getValue(properties.defaultValue, properties.defaultValues); } public visitString(properties: StringFieldPropertiesDto): any { - return properties.defaultValue; + return this.getValue(properties.defaultValue, properties.defaultValues); } public visitTags(properties: TagsFieldPropertiesDto): any { - return properties.defaultValue; + return this.getValue(properties.defaultValue, properties.defaultValues); } public visitUI(_: UIFieldPropertiesDto): any { return null; } + + private getValue(value: any, values?: object) { + if (values && values.hasOwnProperty(this.partitionKey)) { + return values[this.partitionKey]; + } + + return value; + } } \ No newline at end of file diff --git a/frontend/app/shared/state/languages.state.ts b/frontend/app/shared/state/languages.state.ts index 5aa372789..bb43f218e 100644 --- a/frontend/app/shared/state/languages.state.ts +++ b/frontend/app/shared/state/languages.state.ts @@ -58,6 +58,9 @@ export class LanguagesState extends State { public languages = this.project(x => x.languages); + public languagesDtos = + this.project(x => x.languages.map(y => y.language)); + public newLanguages = this.project(x => x.allLanguagesNew);