From 96efe94fba5ccfba5185da6be0ef36ce6c43c3ab Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 22 Jun 2024 11:17:59 +0200 Subject: [PATCH] AI Tools V2 (#1103) * More AI tools. * More tools. --- .../Squidex.Extensions.csproj | 6 +- backend/src/Migrations/Migrations.csproj | 2 +- .../Apps/Json/LanguagesConfigSurrogate.cs | 2 +- .../Apps/LanguagesConfig.cs | 33 ++-- .../Contents/ContentData.cs | 27 +++- .../Contents/ContentFieldData.cs | 8 +- .../Contents/TranslationStatus.cs | 2 +- .../Contents/Updates.cs | 59 +++++++ .../Squidex.Domain.Apps.Core.Model.csproj | 2 +- .../ConvertContent/UpdateValues.cs | 80 +++++++++- ...Squidex.Domain.Apps.Core.Operations.csproj | 6 +- .../ValidateContent/ContentValidator.cs | 6 +- .../DefaultFieldValueValidatorsFactory.cs | 4 +- .../ValidateContent/Undefined.cs | 9 +- .../Validators/FieldValidator.cs | 7 +- .../Validators/ObjectValidator.cs | 14 +- .../Contents/Operations/Extensions.cs | 40 +++-- ...quidex.Domain.Apps.Entities.MongoDb.csproj | 2 +- .../AppChatContext.cs | 15 ++ .../Apps/AppChatTools.cs | 132 ++++++++++++++++ .../DomainObject/ContentDomainObject.cs | 4 +- .../Guards/ValidationExtensions.cs | 4 +- .../Queries/Steps/CalculatePreviewText.cs | 2 +- .../Contents/Queries/Steps/ResolveAssets.cs | 3 +- .../Queries/Steps/ResolveReferences.cs | 2 +- .../Schemas/SchemasChatTool.cs | 71 +++++++++ .../Squidex.Domain.Apps.Entities.csproj | 2 +- .../Squidex.Domain.Apps.Events.csproj | 2 +- .../Squidex.Domain.Users.MongoDb.csproj | 2 +- .../Squidex.Domain.Users.csproj | 4 +- ...quidex.Infrastructure.GetEventStore.csproj | 8 +- .../Squidex.Infrastructure.MongoDb.csproj | 2 +- .../Squidex.Infrastructure.csproj | 18 +-- .../src/Squidex.Shared/Squidex.Shared.csproj | 2 +- backend/src/Squidex.Web/Resources.cs | 2 +- backend/src/Squidex.Web/Squidex.Web.csproj | 2 +- .../Controllers/Apps/Models/AppLanguageDto.cs | 2 +- .../Apps/Models/AppLanguagesDto.cs | 2 +- .../Translations/TranslationsController.cs | 7 +- .../src/Squidex/Config/Domain/AppsServices.cs | 4 + .../Squidex/Config/Domain/SchemasServices.cs | 6 +- backend/src/Squidex/Squidex.csproj | 40 ++--- backend/src/Squidex/appsettings.json | 16 +- .../Model/Apps/LanguagesConfigTests.cs | 16 +- .../Model/Contents/ContentDataTests.cs | 86 ++++++++++- .../ConvertContent/DefaultValuesTests.cs | 6 +- ...Tests.cs => UpdateValuesConverterTests.cs} | 79 +++++++++- .../Scripting/JintScriptEngineHelperTests.cs | 6 +- .../ValidateContent/ContentValidationTests.cs | 88 +++++++++++ .../Squidex.Domain.Apps.Core.Tests.csproj | 2 +- .../Apps/AppChatToolsTests.cs | 146 ++++++++++++++++++ .../Contents/MongoDb/ExtensionsTests.cs | 69 +++++++++ .../Contents/Queries/ConvertDataTests.cs | 16 +- .../Schemas/SchemasChatToolTests.cs | 62 ++++++++ .../Squidex.Domain.Apps.Entities.Tests.csproj | 4 +- .../TestHelpers/GivenContext.cs | 3 + ...reate_events_and_add_language.verified.txt | 2 +- ...ld_create_events_and_add_role.verified.txt | 2 +- ...reate_events_and_add_workflow.verified.txt | 2 +- ...te_events_and_add_contributor.verified.txt | 2 +- ...events_and_update_contributor.verified.txt | 2 +- ..._create_events_and_add_client.verified.txt | 2 +- ...create_events_and_update_plan.verified.txt | 2 +- ...ould_reset_plan_for_free_plan.verified.txt | 2 +- ...create_events_and_update_plan.verified.txt | 2 +- ..._billing_manager_for_callback.verified.txt | 2 +- ..._not_make_update_for_redirect.verified.txt | 2 +- ...ould_reset_plan_for_free_plan.verified.txt | 2 +- ...events_and_set_intitial_state.verified.txt | 2 +- ..._assign_client_as_contributor.verified.txt | 2 +- ...vents_and_update_deleted_flag.verified.txt | 2 +- ...create_events_and_delete_role.verified.txt | 2 +- ...te_events_and_remove_workflow.verified.txt | 2 +- ...events_and_remove_contributor.verified.txt | 2 +- ...reate_events_and_update_image.verified.txt | 2 +- ...te_events_and_remove_language.verified.txt | 2 +- ...eate_events_and_remove_client.verified.txt | 2 +- ...ld_create_events_and_set_team.verified.txt | 2 +- ...ld_create_events_and_set_team.verified.txt | 2 +- ...eate_events_and_update_client.verified.txt | 2 +- ...te_events_and_update_language.verified.txt | 2 +- ...create_events_and_update_role.verified.txt | 2 +- ...ate_event_and_update_settings.verified.txt | 2 +- ...te_events_and_update_workflow.verified.txt | 2 +- ..._update_label_and_description.verified.txt | 2 +- ...reate_events_and_update_image.verified.txt | 2 +- .../Squidex.Domain.Users.Tests.csproj | 2 +- .../Squidex.Infrastructure.Tests.csproj | 2 +- .../Squidex.Web.Tests.csproj | 2 +- .../editors/date-time-editor.component.scss | 4 +- .../pages/internal/chat-menu.component.html | 9 ++ .../pages/internal/chat-menu.component.scss | 7 + .../pages/internal/chat-menu.component.ts | 33 ++++ .../internal/internal-area.component.html | 1 + .../pages/internal/internal-area.component.ts | 2 + .../TestSuite.ApiTests/ContentUpdateTests.cs | 57 +++++++ 96 files changed, 1224 insertions(+), 201 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Contents/Updates.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/AppChatContext.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/AppChatTools.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasChatTool.cs rename backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/{UpdateConverterTests.cs => UpdateValuesConverterTests.cs} (65%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ExtensionsTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs create mode 100644 frontend/src/app/shell/pages/internal/chat-menu.component.html create mode 100644 frontend/src/app/shell/pages/internal/chat-menu.component.scss create mode 100644 frontend/src/app/shell/pages/internal/chat-menu.component.ts diff --git a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj index e33ca1927..e4eb87e85 100644 --- a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -18,8 +18,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,7 +27,7 @@ - + diff --git a/backend/src/Migrations/Migrations.csproj b/backend/src/Migrations/Migrations.csproj index 7934d73f2..ef4b0e8dc 100644 --- a/backend/src/Migrations/Migrations.csproj +++ b/backend/src/Migrations/Migrations.csproj @@ -6,7 +6,7 @@ enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigSurrogate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigSurrogate.cs index eee3e2801..b17eea798 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigSurrogate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigSurrogate.cs @@ -17,7 +17,7 @@ public sealed class LanguagesConfigSurrogate : ISurrogate public void FromSource(LanguagesConfig source) { - Languages = source.Languages.ToDictionary(x => x.Key, source => + Languages = source.Values.ToDictionary(x => x.Key, source => { var surrogate = new LanguageConfigSurrogate(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index b87b0cee6..d17b2762f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -20,7 +20,7 @@ public sealed class LanguagesConfig : IFieldPartitioning }, Language.EN); - private readonly Dictionary languages; + private readonly Dictionary values; private readonly string master; public string Master @@ -30,23 +30,22 @@ public sealed class LanguagesConfig : IFieldPartitioning public IEnumerable AllKeys { - get => languages.Keys; + get => values.Keys; } - public IReadOnlyDictionary Languages + public IReadOnlyDictionary Values { - get => languages; + get => values; } - public LanguagesConfig(Dictionary languages, string master) + public LanguagesConfig(Dictionary values, string master) { - Guard.NotNull(languages); + Guard.NotNull(values); Guard.NotNullOrEmpty(master); - Cleanup(languages, ref master); - - this.languages = languages; + Cleanup(values, ref master); + this.values = values; this.master = master; } @@ -55,7 +54,7 @@ public sealed class LanguagesConfig : IFieldPartitioning { Guard.NotNull(language); - return Build(languages, language); + return Build(values, language); } [Pure] @@ -63,7 +62,7 @@ public sealed class LanguagesConfig : IFieldPartitioning { Guard.NotNull(language); - var newLanguages = new Dictionary(languages) + var newLanguages = new Dictionary(values) { [language] = new LanguageConfig(isOptional, ReadonlyList.Create(fallbacks)) }; @@ -76,7 +75,7 @@ public sealed class LanguagesConfig : IFieldPartitioning { Guard.NotNull(language); - var newLanguages = new Dictionary(languages); + var newLanguages = new Dictionary(values); newLanguages.Remove(language); @@ -102,7 +101,7 @@ public sealed class LanguagesConfig : IFieldPartitioning private bool EqualLanguages(Dictionary newLanguages) { - return newLanguages.EqualsDictionary(languages); + return newLanguages.EqualsDictionary(values); } private void Cleanup(Dictionary newLanguages, ref string newMaster) @@ -152,7 +151,7 @@ public sealed class LanguagesConfig : IFieldPartitioning public string? GetName(string key) { - if (key != null && languages.ContainsKey(key)) + if (key != null && values.ContainsKey(key)) { return Language.GetLanguage(key).EnglishName; } @@ -162,7 +161,7 @@ public sealed class LanguagesConfig : IFieldPartitioning public bool IsOptional(string key) { - if (key != null && languages.TryGetValue(key, out var value)) + if (key != null && values.TryGetValue(key, out var value)) { return value.IsOptional; } @@ -178,7 +177,7 @@ public sealed class LanguagesConfig : IFieldPartitioning { yield return key; } - else if (languages.TryGetValue(key, out var config)) + else if (values.TryGetValue(key, out var config)) { yield return key; @@ -197,7 +196,7 @@ public sealed class LanguagesConfig : IFieldPartitioning public bool Contains(string key) { - return key != null && languages.ContainsKey(key); + return key != null && values.ContainsKey(key); } public override string ToString() diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs index ed7cb6b38..f331814e4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs @@ -16,13 +16,13 @@ public sealed class ContentData : Dictionary, IEquata { } - public ContentData(ContentData source) - : base(source, StringComparer.Ordinal) + public ContentData(int capacity) + : base(capacity, StringComparer.Ordinal) { } - public ContentData(int capacity) - : base(capacity, StringComparer.Ordinal) + public ContentData(IDictionary source) + : base(source, StringComparer.Ordinal) { } @@ -96,16 +96,29 @@ public sealed class ContentData : Dictionary, IEquata continue; } - var targetFieldData = target.GetOrAdd(fieldName, _ => new ContentFieldData()); + if (Updates.IsUnset(sourceFieldData)) + { + target.Remove(fieldName); + continue; + } + + var targetFieldData = target.GetOrAdd(fieldName, _ => []); if (targetFieldData == null) { continue; } - foreach (var (partition, value) in sourceFieldData) + foreach (var (partition, sourceValue) in sourceFieldData) { - targetFieldData[partition] = value; + if (Updates.IsUnset(sourceValue)) + { + targetFieldData.Remove(partition); + } + else + { + targetFieldData[partition] = sourceValue; + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs index bd25be596..421b31251 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs @@ -23,13 +23,9 @@ public sealed class ContentFieldData : Dictionary, IEquatable { } - public ContentFieldData(ContentFieldData source) - : base(source.Count, StringComparer.OrdinalIgnoreCase) + public ContentFieldData(IDictionary source) + : base(source, StringComparer.Ordinal) { - foreach (var (key, value) in source) - { - this[key] = value; - } } public bool TryGetNonNull(string key, [MaybeNullWhen(false)] out JsonValue result) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs index f17edba99..a5c65d2b3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs @@ -29,7 +29,7 @@ public class TranslationStatus : Dictionary Guard.NotNull(schema); Guard.NotNull(languages); - var result = new TranslationStatus(languages.Languages.Count); + var result = new TranslationStatus(languages.Values.Count); var localizedFields = schema.Fields.Where(x => x.Partitioning == Partitioning.Language).ToList(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Updates.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Updates.cs new file mode 100644 index 000000000..5333e88af --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Updates.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Core.Contents; + +public static class Updates +{ + public static bool IsUnset(object? value) + { + if (value is JsonValue json) + { + return IsUnset(json.Value); + } + + return value is IReadOnlyDictionary obj && IsUnset(obj); + } + + public static bool IsUnset(IReadOnlyDictionary? obj) + { + return + obj is { Count: 1 } && + obj.TryGetValue("$unset", out var item) && + Equals(item.Value, true); + } + + public static bool IsUpdate(object? value, out string expression) + { + expression = null!; + + if (value is JsonValue json) + { + return IsUpdate(json.Value, out expression); + } + + return value is IReadOnlyDictionary obj && IsUpdate(obj, out expression); + } + + public static bool IsUpdate(IReadOnlyDictionary? obj, out string expression) + { + expression = null!; + + if (obj is { Count: > 0 } && + obj.TryGetValue("$update", out var item) && + item.Value is string e && + !string.IsNullOrWhiteSpace(e)) + { + expression = e; + return true; + } + + return false; + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index 9cb29d38f..a3231a160 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -12,7 +12,7 @@ True - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/UpdateValues.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/UpdateValues.cs index 00ba891cf..ca22bf436 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/UpdateValues.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/UpdateValues.cs @@ -12,44 +12,110 @@ using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.ConvertContent; -public sealed class UpdateValues : IContentValueConverter +public sealed class UpdateValues : IContentValueConverter, IContentDataConverter { private readonly ContentData existingData; private readonly IScriptEngine scriptEngine; + private readonly bool canUnset; private ScriptVars? vars; - public UpdateValues(ContentData existingData, IScriptEngine scriptEngine) + public UpdateValues(ContentData existingData, IScriptEngine scriptEngine, bool canUnset) { this.existingData = existingData; this.scriptEngine = scriptEngine; + this.canUnset = canUnset; + } + + public void ConvertDataBefore(Schema schema, ContentData source) + { + // Avoid unnecessary allocations if nothing has been changed, which is the default. + List? toRemove = null; + List<(string, ContentFieldData)>? toReplace = null; + + foreach (var (key, value) in source) + { + if (canUnset && Updates.IsUnset(value)) + { + toRemove ??= []; + toRemove.Add(key); + } + + if (Updates.IsUpdate(value, out var expression)) + { + var options = new ScriptOptions { Readonly = true }; + + // Reuse the vars to save allocations. + vars ??= new ScriptVars + { + ["$data"] = existingData + }; + + // Give access to the current update statement to carry extra values from the request. + vars["$self"] = value; + + // Put the expression in brackets to return an object directly. + var result = scriptEngine.Execute(vars, $"({expression})", options); + + if (result.Value is JsonObject obj) + { + var replacement = new ContentFieldData(obj); + + if (!replacement.Equals(value)) + { + toReplace ??= []; + toReplace.Add((key, replacement)); + } + } + } + } + + if (toRemove != null) + { + foreach (var key in toRemove) + { + source.Remove(key); + } + } + + if (toReplace != null) + { + foreach (var (key, value) in toReplace) + { + source[key] = value; + } + } } public (bool Remove, JsonValue) ConvertValue(IField field, JsonValue source, IField? parent) { - if (source.Value is not JsonObject jsonObject) + if (source.Value is not JsonObject obj) { return (false, source); } - if (jsonObject.TryGetValue("$unset", out var value1) && !Equals(value1.Value, false)) + if (canUnset && Updates.IsUnset(obj)) { return (true, source); } - if (!jsonObject.TryGetValue("$update", out var value2) || value2.Value is not string update) + if (!Updates.IsUpdate(obj, out var expression)) { return (false, source); } var options = new ScriptOptions { Readonly = true }; + // Reuse the vars to save allocations. vars ??= new ScriptVars { ["$data"] = existingData, - ["$self"] = jsonObject }; - var result = scriptEngine.Execute(vars, update, options); + // Give access to the current update statement to carry extra values from the request. + vars["$self"] = obj; + + // Put the expression in brackets to return an object directly. + var result = scriptEngine.Execute(vars, $"({expression})", options); return (false, result); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index c62d0a01d..50fc057fe 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -21,15 +21,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs index c36d80657..e32e04b0a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -110,13 +110,13 @@ public sealed class ContentValidator var valueValidator = CreateValueValidator(field); var partitioning = partitionResolver(field.Partitioning); - var partitioningValidators = new Dictionary(); + var partitions = new Dictionary(); foreach (var partitionKey in partitioning.AllKeys) { var optional = partitioning.IsOptional(partitionKey); - partitioningValidators[partitionKey] = (optional, valueValidator); + partitions[partitionKey] = (optional, valueValidator); } var typeName = partitioning.ToString()!; @@ -124,7 +124,7 @@ public sealed class ContentValidator return new AggregateValidator( CreateFieldValidators(field) .Union(Enumerable.Repeat( - new ObjectValidator(partitioningValidators, isPartial, typeName), 1))); + new ObjectValidator(partitions, isPartial, typeName), 1))); } private IValidator CreateValueValidator(IField field) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs index c8535bef5..dd9f7a35c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs @@ -36,7 +36,9 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor : IValidator public void Validate(object? value, ValidationContext context) { + var originalValue = value; + if (value.IsNullOrUndefined()) { value = DefaultValue; @@ -48,9 +51,10 @@ public sealed class ObjectValidator : IValidator if (!values.TryGetValue(name, out var nestedValue)) { - if (isPartial) + // If the original value was unset, we have to validate the children for required values. + if (isPartial && !originalValue.IsUnset()) { - return; + continue; } } else @@ -58,6 +62,12 @@ public sealed class ObjectValidator : IValidator fieldValue = nestedValue!; } + // Use a special null values for unsets so we can treat them as null for required validators. + if (Updates.IsUnset(fieldValue)) + { + fieldValue = Undefined.Unset; + } + var fieldContext = context.Nested(name, field.IsOptional); field.Validator.Validate(fieldValue, fieldContext); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs index 4976c5772..35bb6d9e3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs @@ -29,11 +29,11 @@ public static class Extensions .Add("$expr", new BsonDocument() .Add("$eq", new BsonArray { "$_id", "$$id" })); - private static Dictionary propertyMap; + private static Dictionary metaFields; - public static IReadOnlyDictionary PropertyMap + private static IReadOnlyDictionary MetaFields { - get => propertyMap ??= + get => metaFields ??= BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).AllMemberMaps .Where(x => x.MemberName != nameof(MongoContentEntity.NewData) && @@ -173,30 +173,44 @@ public static class Extensions return find.Project(BuildProjection2(fields)); } - private static ProjectionDefinition BuildProjection2(IEnumerable? fields) + public static ProjectionDefinition BuildProjection2(IEnumerable? fields) { var projector = Builders.Projection; var projections = new List>(); if (fields?.Any() == true) { - var dataPrefix = Field.Of(x => nameof(x.Data)); - - foreach (var field in fields) + static IEnumerable GetDataFields(IEnumerable fields) { - var dataField = field; + var dataPrefix = Field.Of(x => nameof(x.Data)); - if (FieldNames.IsDataField(field, out var fieldName)) + foreach (var field in fields) { - dataField = fieldName; - } + var dataField = + FieldNames.IsDataField(field, out var fieldName) ? + fieldName : + field; - projections.Add(projector.Include($"{dataPrefix}.{dataField}")); + yield return $"{dataPrefix}.{dataField}"; + } } - foreach (var field in PropertyMap.Values) + var addedFields = new List(); + + // Sort the fields to start with prefixes first. + var allFields = GetDataFields(fields).Union(MetaFields.Values).OrderBy(x => x); + + foreach (var field in allFields) { + // If there is at least one field that is a prefix of the current field, we cannot add that. + if (addedFields.Exists(x => field.StartsWith(x, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + projections.Add(projector.Include(field)); + + addedFields.Add(field); } } else diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index 0c2757236..d3de1ee51 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppChatContext.cs b/backend/src/Squidex.Domain.Apps.Entities/AppChatContext.cs new file mode 100644 index 000000000..786a50fcb --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/AppChatContext.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.AI; + +namespace Squidex.Domain.Apps.Entities; + +public sealed class AppChatContext : ChatContext +{ + required public Context BaseContext { get; init; } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppChatTools.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppChatTools.cs new file mode 100644 index 000000000..442520087 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppChatTools.cs @@ -0,0 +1,132 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Runtime.CompilerServices; +using Squidex.AI; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Infrastructure.Json; +using Squidex.Shared; + +namespace Squidex.Domain.Apps.Entities.Apps; + +public sealed class AppChatTools : IChatToolProvider +{ + private readonly IJsonSerializer serializer; + private readonly IUrlGenerator urlGenerator; + + public AppChatTools(IJsonSerializer serializer, IUrlGenerator urlGenerator) + { + this.serializer = serializer; + this.urlGenerator = urlGenerator; + } + + public async IAsyncEnumerable GetToolsAsync(ChatContext chatContext, + [EnumeratorCancellation] CancellationToken ct) + { + if (chatContext is not AppChatContext appContext) + { + yield break; + } + + var context = appContext.BaseContext; + + await Task.Yield(); + + if (context.Allows(PermissionIds.AppClientsRead)) + { + yield return new DelegateChatTool( + new ToolSpec("clients", "Clients", "Provides the clients for the Squidex App."), + (_, ct) => + { + var result = new + { + Clients = context.App.Clients.Select(x => + new + { + Id = x.Key, + ClientId = $"{context.App.Name}:{x.Key}", + ClientSecret = "obfuscated", + x.Value.Role + }), + Url = urlGenerator.ClientsUI(context.App.NamedId()) + }; + + var json = serializer.Serialize(result, true); + + return Task.FromResult(json); + }); + } + + if (context.Allows(PermissionIds.AppLanguagesRead)) + { + yield return new DelegateChatTool( + new ToolSpec("languages", "Languages", "Provides the languages for the Squidex App."), + (_, ct) => + { + var result = new + { + Languages = context.App.Languages.Values.Select(x => + new + { + Iso2Code = x.Key, + IsMaster = context.App.Languages.Master.Equals(x.Key), + x.Value.IsOptional + }), + Url = urlGenerator.LanguagesUI(context.App.NamedId()) + }; + + var json = serializer.Serialize(result, true); + + return Task.FromResult(json); + }); + } + + if (context.Allows(PermissionIds.AppRolesRead)) + { + yield return new DelegateChatTool( + new ToolSpec("roles", "Roles", "Provides the roles for the Squidex App."), + (_, ct) => + { + var result = new + { + Roles = context.App.Roles.Custom.Select(x => + new + { + x.Name + }), + Url = urlGenerator.RolesUI(context.App.NamedId()) + }; + + var json = serializer.Serialize(result, true); + + return Task.FromResult(json); + }); + } + + if (context.Allows(PermissionIds.AppPlansRead)) + { + yield return new DelegateChatTool( + new ToolSpec("plan", "Plan", "Provides the plan for the Squidex App."), + (_, ct) => + { + var result = new + { + Plan = new + { + Name = context.App.Plan?.PlanId, + }, + Url = urlGenerator.PlansUI(context.App.NamedId()) + }; + + var json = serializer.Serialize(result, true); + + return Task.FromResult(json); + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs index c11f47310..13f2a38bc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs @@ -322,7 +322,7 @@ public partial class ContentDomainObject : DomainObject operation.MustHavePermission(PermissionIds.AppContentsUpdate); operation.MustHaveData(c.Data); - var newData = operation.InvokeUpdates(c.Data, Snapshot.EditingData); + var newData = operation.InvokeUpdates(c.Data, Snapshot.EditingData, true); if (!c.DoNotValidate) { @@ -363,7 +363,7 @@ public partial class ContentDomainObject : DomainObject operation.MustHavePermission(PermissionIds.AppContentsUpdate); operation.MustHaveData(c.Data); - c.Data = operation.InvokeUpdates(c.Data, Snapshot.EditingData); + c.Data = operation.InvokeUpdates(c.Data, Snapshot.EditingData, false); if (!c.DoNotValidate) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs index e8cb501c3..4ed82d679 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs @@ -98,13 +98,13 @@ public static class ValidationExtensions return converter.Convert(data); } - public static ContentData InvokeUpdates(this ContentOperation operation, ContentData data, ContentData currentData) + public static ContentData InvokeUpdates(this ContentOperation operation, ContentData data, ContentData currentData, bool canUnset) { var converter = new ContentConverter( operation.Components, operation.Schema); - converter.Add(new UpdateValues(currentData, operation.Resolve())); + converter.Add(new UpdateValues(currentData, operation.Resolve(), canUnset)); return converter.Convert(data); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculatePreviewText.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculatePreviewText.cs index bfd312b2e..4129e6ff2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculatePreviewText.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculatePreviewText.cs @@ -41,7 +41,7 @@ public sealed class CalculatePreviewText : IContentEnricherStep content.ReferenceData ??= []; - var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => [])!; foreach (var (partitionKey, partitionValue) in fieldData) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs index 58a51b801..51ea63a82 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs @@ -7,7 +7,6 @@ using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Assets; -using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Assets; @@ -72,7 +71,7 @@ public sealed class ResolveAssets : IContentEnricherStep { content.ReferenceData ??= []; - var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => [])!; if (content.Data.TryGetValue(field.Name, out var fieldData) && fieldData != null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs index e6dde476d..5a68fe24a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs @@ -74,7 +74,7 @@ public sealed class ResolveReferences : IContentEnricherStep { content.ReferenceData ??= []; - var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!; + var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => [])!; try { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasChatTool.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasChatTool.cs new file mode 100644 index 000000000..b5b0e623f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasChatTool.cs @@ -0,0 +1,71 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Runtime.CompilerServices; +using Squidex.AI; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json; +using Squidex.Shared; + +namespace Squidex.Domain.Apps.Entities.Schemas; + +public sealed class SchemasChatTool : IChatToolProvider +{ + private readonly IAppProvider appProvider; + private readonly IJsonSerializer serializer; + private readonly IUrlGenerator urlGenerator; + + public SchemasChatTool(IAppProvider appProvider, IJsonSerializer serializer, IUrlGenerator urlGenerator) + { + this.appProvider = appProvider; + this.serializer = serializer; + this.urlGenerator = urlGenerator; + } + + public async IAsyncEnumerable GetToolsAsync(ChatContext chatContext, + [EnumeratorCancellation] CancellationToken ct) + { + if (chatContext is not AppChatContext appContext) + { + yield break; + } + + var context = appContext.BaseContext; + + await Task.Yield(); + + if (context.Allows(PermissionIds.AppSchemasRead)) + { + yield return new DelegateChatTool( + new ToolSpec("schemas", "Schemas", "Provides the schemas for the Squidex App."), + async (_, ct) => + { + var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); + + var result = new + { + Schemas = schemas.Select(x => + new + { + x.Name, + x.IsPublished, + x.Type, + FieldCount = x.Fields.Count, + Url = urlGenerator.SchemaUI(context.App.NamedId(), x.NamedId()) + }), + Url = urlGenerator.SchemasUI(context.App.NamedId()) + }; + + var json = serializer.Serialize(result, true); + + return json; + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index d05c6a32c..af517f4e4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -27,7 +27,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj index c7f203cb6..0d14544a3 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index 92882e229..0fe32e5bb 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 213163320..194049675 100644 --- a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -18,11 +18,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj index 643309d89..15345960b 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj +++ b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj @@ -11,11 +11,11 @@ True - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index d450c763b..80d65af98 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index abf5aba1b..ace56ef26 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -13,23 +13,23 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - - - - - - + + + + + + diff --git a/backend/src/Squidex.Shared/Squidex.Shared.csproj b/backend/src/Squidex.Shared/Squidex.Shared.csproj index 6a048ea3c..e5672b2f5 100644 --- a/backend/src/Squidex.Shared/Squidex.Shared.csproj +++ b/backend/src/Squidex.Shared/Squidex.Shared.csproj @@ -10,7 +10,7 @@ True - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Web/Resources.cs b/backend/src/Squidex.Web/Resources.cs index aec0995fd..ef2936711 100644 --- a/backend/src/Squidex.Web/Resources.cs +++ b/backend/src/Squidex.Web/Resources.cs @@ -16,7 +16,7 @@ namespace Squidex.Web; public sealed class Resources { - private readonly Dictionary<(string Id, string Schema), bool> permissions = new Dictionary<(string, string), bool>(); + private readonly Dictionary<(string Id, string Schema), bool> permissions = []; // Contents public bool CanReadContent(string schema) => Can(PermissionIds.AppContentsReadOwn, schema); diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj index dc94a13c5..d938987d5 100644 --- a/backend/src/Squidex.Web/Squidex.Web.csproj +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs index da0438ec6..46e9812df 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs @@ -64,7 +64,7 @@ public sealed class AppLanguageDto : Resource resources.Url(x => nameof(x.PutLanguage), values)); } - if (resources.CanDeleteLanguage && app.Languages.Languages.Count > 1) + if (resources.CanDeleteLanguage && app.Languages.Values.Count > 1) { AddDeleteLink("delete", resources.Url(x => nameof(x.DeleteLanguage), values)); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs index 4ac978110..f1b90efaf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs @@ -23,7 +23,7 @@ public sealed class AppLanguagesDto : Resource var result = new AppLanguagesDto { - Items = config.Languages + Items = config.Values .Select(x => AppLanguageDto.FromDomain(x.Key, x.Value, config)) .Select(x => x.CreateLinks(resources, app)) .OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs index 36cd5de6d..81395f2b8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -12,6 +12,7 @@ using NSwag.Annotations; using Squidex.AI; using Squidex.Areas.Api.Controllers.Translations.Models; using Squidex.Assets; +using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Text.Translations; @@ -92,9 +93,11 @@ public sealed class TranslationsController : ApiController Prompt = request.Prompt }; - var context = new ChatContext + var context = new AppChatContext { - User = User + User = User, + // Use a special context to provide access to the app. + BaseContext = Context, }; return new FileCallbackResult("text/event-stream", async (body, range, ct) => diff --git a/backend/src/Squidex/Config/Domain/AppsServices.cs b/backend/src/Squidex/Config/Domain/AppsServices.cs index 046dce7b8..3d0c40edf 100644 --- a/backend/src/Squidex/Config/Domain/AppsServices.cs +++ b/backend/src/Squidex/Config/Domain/AppsServices.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.Extensions.Options; +using Squidex.AI; using Squidex.Areas.Api.Controllers.UI; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities; @@ -51,6 +52,9 @@ public static class AppsServices services.AddSingletonAs() .As().As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/SchemasServices.cs b/backend/src/Squidex/Config/Domain/SchemasServices.cs index 4f0065461..432421daf 100644 --- a/backend/src/Squidex/Config/Domain/SchemasServices.cs +++ b/backend/src/Squidex/Config/Domain/SchemasServices.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.AI; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Search; @@ -15,9 +16,12 @@ public static class SchemasServices { public static void AddSquidexSchemas(this IServiceCollection services) { - services.AddTransientAs() + services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); } diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index c240d277f..32c0484e6 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -38,22 +38,22 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + - - + + @@ -64,19 +64,19 @@ - - - - - - - - - + + + + + + + + + - - - + + + diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 611f88e9d..f538fa3d1 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -690,10 +690,9 @@ "defaults": { "systemMessages": [ - "You are a bot to generate text content.", + "You are a bot to help with all support requests related to Squidex.", "Say hello to the user and explain him about your capabilities in a single, short sentence." - ], - "tools": [] + ] }, "configurations": { @@ -701,7 +700,16 @@ "systemMessages": [ "You are a bot to generate images.", "Say hello to the user and explain him the user about your capabilities in a single, short sentence." - ] + ], + "tools": [ "dall-e" ] + }, + + "text": { + "systemMessages": [ + "You are a bot to generate text content.", + "Say hello to the user and explain him about your capabilities in a single, short sentence." + ], + "tools": [] } } }, diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs index 9700cd101..d458ae373 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs @@ -87,7 +87,7 @@ public class LanguagesConfigTests [Fact] public void Should_create_initial_config() { - config_0.Languages.Should().BeEquivalentTo( + config_0.Values.Should().BeEquivalentTo( new Dictionary { [Language.EN] = new LanguageConfig() @@ -106,7 +106,7 @@ public class LanguagesConfigTests .Set(Language.IT, true, Language.ES) .MakeMaster(Language.DE); - config.Languages.Should().BeEquivalentTo( + config.Values.Should().BeEquivalentTo( new Dictionary { [Language.EN] = new LanguageConfig(), @@ -141,7 +141,7 @@ public class LanguagesConfigTests .Set(Language.IT, true, Language.ES) .MakeMaster(Language.IT); - config.Languages.Should().BeEquivalentTo( + config.Values.Should().BeEquivalentTo( new Dictionary { [Language.EN] = new LanguageConfig(), @@ -182,7 +182,7 @@ public class LanguagesConfigTests var config_2 = config_1.Set(Language.IT); var config_3 = config_2.Remove(Language.DE); - config_3.Languages.Should().BeEquivalentTo( + config_3.Values.Should().BeEquivalentTo( new Dictionary { [Language.EN] = new LanguageConfig(), @@ -199,7 +199,7 @@ public class LanguagesConfigTests var config_2 = config_1.Set(Language.IT, true, Language.UK); var config_3 = config_2.Remove(Language.DE); - config_3.Languages.Should().BeEquivalentTo( + config_3.Values.Should().BeEquivalentTo( new Dictionary { [Language.EN] = new LanguageConfig(), @@ -224,7 +224,7 @@ public class LanguagesConfigTests var config_2 = config_1.Set(Language.IT); var config_3 = config_2.Remove(Language.EN); - config_3.Languages.Should().BeEquivalentTo( + config_3.Values.Should().BeEquivalentTo( new Dictionary { [Language.DE] = new LanguageConfig(), @@ -248,7 +248,7 @@ public class LanguagesConfigTests var config_1 = config_0.Set(Language.IT); var config_2 = config_1.Set(Language.IT, true, Language.EN); - config_2.Languages.Should().BeEquivalentTo( + config_2.Values.Should().BeEquivalentTo( new Dictionary { [Language.EN] = new LanguageConfig(), @@ -265,7 +265,7 @@ public class LanguagesConfigTests var config_2 = config_1.Set(Language.IT); var config_3 = config_2.Set(Language.IT, true, Language.EN, Language.IT, Language.DE); - config_3.Languages.Should().BeEquivalentTo( + config_3.Values.Should().BeEquivalentTo( new Dictionary { [Language.EN] = new LanguageConfig(), diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs index d26b1307c..da6800dcb 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs @@ -6,13 +6,14 @@ // ========================================================================== using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.Model.Contents; public class ContentDataTests { [Fact] - public void Should_return_same_content_if_merging_same_references() + public void Should_return_same_data_if_merging_same_references() { var source = new ContentData() @@ -29,7 +30,7 @@ public class ContentDataTests } [Fact] - public void Should_merge_two_name_models() + public void Should_merge_data() { var lhs = new ContentData() @@ -73,9 +74,86 @@ public class ContentDataTests } [Fact] - public void Should_be_equal_if_data_have_same_structure() + public void Should_unset_field_value_when_merging() { + var rhs = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(1)) + .AddField("field2", + new ContentFieldData() + .AddLocalized("de", 2) + .AddLocalized("it", 2)); + var lhs = + new ContentData() + .AddField("field2", + new ContentFieldData() + .AddLocalized("it", + JsonValue.Object() + .Add("$unset", true)) + .AddLocalized("en", 3)); + + var expected = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(1)) + .AddField("field2", + new ContentFieldData() + .AddLocalized("de", 2) + .AddLocalized("en", 3)); + + var actual = lhs.MergeInto(rhs); + + Assert.Equal(expected, actual); + Assert.NotSame(expected, rhs); + Assert.NotSame(expected, lhs); + } + + [Fact] + public void Should_unset_field_when_merging() + { + var rhs = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(1)) + .AddField("field2", + new ContentFieldData() + .AddLocalized("de", 2) + .AddLocalized("it", 2)); + + var lhs = + new ContentData() + .AddField("field2", + new ContentFieldData() + .AddLocalized("$unset", true)) + .AddField("field3", + new ContentFieldData() + .AddInvariant(4)); + + var expected = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddInvariant(1)) + .AddField("field3", + new ContentFieldData() + .AddInvariant(4)); + + var actual = lhs.MergeInto(rhs); + + Assert.Equal(expected, actual); + Assert.NotSame(expected, rhs); + Assert.NotSame(expected, lhs); + } + + [Fact] + public void Should_be_equal_if_data_have_same_structure() + { + var rhs = new ContentData() .AddField("field1", new ContentFieldData() @@ -84,7 +162,7 @@ public class ContentDataTests new ContentFieldData() .AddInvariant(2)); - var rhs = + var lhs = new ContentData() .AddField("field1", new ContentFieldData() diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/DefaultValuesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/DefaultValuesTests.cs index 1d72b5abe..ba82127bc 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/DefaultValuesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/DefaultValuesTests.cs @@ -74,7 +74,7 @@ public class DefaultValuesTests .AddInvariant(now.ToString())) .AddField("myBoolean", new ContentFieldData() - .AddInvariant(JsonValue.True)) + .AddInvariant(true)) .AddField("myArray", new ContentFieldData() .AddInvariant( @@ -154,7 +154,7 @@ public class DefaultValuesTests .AddInvariant(now.ToString())) .AddField("myBoolean", new ContentFieldData() - .AddInvariant(JsonValue.True)) + .AddInvariant(true)) .AddField("myArray", new ContentFieldData() .AddInvariant( @@ -196,7 +196,7 @@ public class DefaultValuesTests .AddInvariant(now.ToString())) .AddField("myBoolean", new ContentFieldData() - .AddInvariant(JsonValue.True)) + .AddInvariant(true)) .AddField("myArray", new ContentFieldData() .AddInvariant( diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateConverterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateValuesConverterTests.cs similarity index 65% rename from backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateConverterTests.cs rename to backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateValuesConverterTests.cs index d9c379aad..5d4fc1051 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateConverterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/UpdateValuesConverterTests.cs @@ -15,11 +15,11 @@ using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.Operations.ConvertContent; -public class UpdateConverterTests +public class UpdateValuesConverterTests { private readonly IScriptEngine scriptEngine; - public UpdateConverterTests() + public UpdateValuesConverterTests() { scriptEngine = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), Options.Create(new JintScriptOptions @@ -30,7 +30,7 @@ public class UpdateConverterTests } [Fact] - public void Should_update() + public void Should_update_value() { var field1 = Fields.Number(1, "number1", Partitioning.Invariant); @@ -54,7 +54,42 @@ public class UpdateConverterTests var actual = new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new UpdateValues(existing, scriptEngine)) + .Add(new UpdateValues(existing, scriptEngine, true)) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 43)); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_update_field() + { + var field1 = Fields.Number(1, "number1", Partitioning.Invariant); + + var schema = + new Schema { Name = "my-schema" } + .AddField(field1); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("$update", "{ \"en\": $data.number1.en + 1 }")); + + var existing = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 42)); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new UpdateValues(existing, scriptEngine, true)) .Convert(source); var expected = @@ -92,7 +127,7 @@ public class UpdateConverterTests var actual = new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new UpdateValues(existing, scriptEngine)) + .Add(new UpdateValues(existing, scriptEngine, true)) .Convert(source); var expected = @@ -129,13 +164,43 @@ public class UpdateConverterTests var actual = new ContentConverter(ResolvedComponents.Empty, schema) - .Add(new UpdateValues(existing, scriptEngine)) + .Add(new UpdateValues(existing, scriptEngine, true)) .Convert(source); var expected = + new ContentData() + .AddField(field1.Name, []); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_unset_field() + { + var field1 = Fields.Number(1, "number1", Partitioning.Invariant); + + var schema = + new Schema { Name = "my-schema" } + .AddField(field1); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("$unset", true)); + + var existing = new ContentData() .AddField(field1.Name, - new ContentFieldData()); + new ContentFieldData() + .AddLocalized("en", 42)); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new UpdateValues(existing, scriptEngine, true)) + .Convert(source); + + var expected = new ContentData(); Assert.Equal(expected, actual); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs index c3b41da06..79cf0a4ec 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs @@ -623,7 +623,7 @@ public class JintScriptEngineHelperTests : IClassFixture A.That.Matches(x => x.Prompt == "prompt"), A._, A._)) - .Returns(new ChatResult { Content = "Generated", Metadata = new ChatMetadata() }); + .Returns(new ChatResult { Content = "Generated", Metadata = new ChatMetadata(), Tools = [] }); var vars = new ScriptVars { @@ -639,7 +639,7 @@ public class JintScriptEngineHelperTests : IClassFixture Assert.Equal("Generated", actual.ToString()); - A.CallTo(() => chatAgent.StopConversationAsync(A._, A._)) + A.CallTo(() => chatAgent.StopConversationAsync(A._, A._, A._)) .MustNotHaveHappened(); } @@ -666,7 +666,7 @@ public class JintScriptEngineHelperTests : IClassFixture A.CallTo(() => chatAgent.PromptAsync(A._, A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => chatAgent.StopConversationAsync(A._, A._)) + A.CallTo(() => chatAgent.StopConversationAsync(A._, A._, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index 5ea3a2923..22a447f52 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -453,4 +453,92 @@ public class ContentValidationTests : IClassFixture Assert.Empty(errors); } + + [Fact] + public async Task Should_validate_partial_unset_field_as_null() + { + schema = schema.AddString(1, "myField", Partitioning.Invariant, + new StringFieldProperties { IsRequired = true }); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("$unset", true)); + + await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Field is required.", "myField.iv") + }); + } + + [Fact] + public async Task Should_handle_partial_unset_field_value_as_null() + { + schema = schema.AddString(1, "myField", Partitioning.Invariant, + new StringFieldProperties { IsRequired = true }); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("iv", + JsonValue.Object() + .Add("$unset", true))); + + await data.ValidatePartialAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Field is required.", "myField.iv") + }); + } + + [Fact] + public async Task Should_validate_unset_field_as_null() + { + schema = schema.AddString(1, "myField", Partitioning.Invariant, + new StringFieldProperties { IsRequired = true }); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("$unset", true)); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Field is required.", "myField.iv") + }); + } + + [Fact] + public async Task Should_handle_unset_field_value_as_null() + { + schema = schema.AddString(1, "myField", Partitioning.Invariant, + new StringFieldProperties { IsRequired = true }); + + var data = + new ContentData() + .AddField("myField", + new ContentFieldData() + .AddLocalized("iv", + JsonValue.Object() + .Add("$unset", true))); + + await data.ValidateAsync(languages.ToResolver(), errors, schema); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Field is required.", "myField.iv") + }); + } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 6a77f405e..a9911ea7e 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs new file mode 100644 index 000000000..69c2ae947 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs @@ -0,0 +1,146 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Shared; + +namespace Squidex.Domain.Apps.Entities.Apps; + +public class AppChatToolsTests : GivenContext +{ + private readonly IUrlGenerator urlGenerator = A.Fake(); + private readonly AppChatTools sut; + + public AppChatToolsTests() + { + sut = new AppChatTools(TestUtils.DefaultSerializer, urlGenerator); + } + + [Fact] + public async Task Should_return_clients_if_user_has_permission() + { + App = App with + { + Clients = App.Clients.Add("default", "secret") + }; + + var chatContext = new AppChatContext + { + BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppClientsRead, App.Name).Id) + }; + + var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); + + Assert.NotNull(tool); + Assert.Equal("clients", tool.Spec.Name); + Assert.Equal("Clients", tool.Spec.DisplayName); + + var result = await tool.ExecuteAsync(null!, default); + + Assert.Contains($"{App.Name}:default", result); + + A.CallTo(() => urlGenerator.ClientsUI(AppId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_languages_if_user_has_permission() + { + App = App with + { + Languages = App.Languages.Set(Language.DE) + }; + + var chatContext = new AppChatContext + { + BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppLanguages, App.Name).Id) + }; + + var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); + + Assert.NotNull(tool); + Assert.Equal("languages", tool.Spec.Name); + Assert.Equal("Languages", tool.Spec.DisplayName); + + var result = await tool.ExecuteAsync(null!, default); + + Assert.Contains($"\"de\"", result); + + A.CallTo(() => urlGenerator.LanguagesUI(AppId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_roles_if_user_has_permission() + { + App = App with + { + Roles = App.Roles.Add("viewers") + }; + + var chatContext = new AppChatContext + { + BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppRoles, App.Name).Id) + }; + + var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); + + Assert.NotNull(tool); + Assert.Equal("roles", tool.Spec.Name); + Assert.Equal("Roles", tool.Spec.DisplayName); + + var result = await tool.ExecuteAsync(null!, default); + + Assert.Contains($"viewers", result); + + A.CallTo(() => urlGenerator.RolesUI(AppId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_plan_if_user_has_permission() + { + App = App with + { + Plan = new AssignedPlan(User, "Business") + }; + + var chatContext = new AppChatContext + { + BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppPlans, App.Name).Id) + }; + + var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); + + Assert.NotNull(tool); + Assert.Equal("plan", tool.Spec.Name); + Assert.Equal("Plan", tool.Spec.DisplayName); + + var result = await tool.ExecuteAsync(null!, default); + + Assert.Contains($"Business", result); + + A.CallTo(() => urlGenerator.PlansUI(AppId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_return_tools_if_user_no_permission() + { + var chatContext = new AppChatContext + { + BaseContext = FrontendContext + }; + + var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); + + Assert.Null(tool); + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ExtensionsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ExtensionsTests.cs new file mode 100644 index 000000000..682390102 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ExtensionsTests.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.MongoDb.Contents; +using ExtensionSut = Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations.Extensions; + +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; + +public class ExtensionsTests +{ + public ExtensionsTests() + { + MongoContentEntity.RegisterClassMap(); + } + + [Fact] + public void Should_build_projection_without_fields() + { + var projection = ExtensionSut.BuildProjection2(null); + + AssertProjection(projection, "{ 'dd' : 0 }"); + } + + [Fact] + public void Should_build_projection_with_data_prefix() + { + var projection = ExtensionSut.BuildProjection2(["data.myField"]); + + AssertProjection(projection, "{ '_ai' : 1, '_id' : 1, '_si' : 1, 'ai' : 1, 'cb' : 1, 'ct' : 1, 'dl' : 1, 'do.myField' : 1, 'id' : 1, 'is' : 1, 'mb' : 1, 'mt' : 1, 'ns' : 1, 'rf' : 1, 'sa' : 1, 'si' : 1, 'sj' : 1, 'ss' : 1, 'ts' : 1, 'vs' : 1 }"); + } + + [Fact] + public void Should_build_projection_without_data_prefix() + { + var projection = ExtensionSut.BuildProjection2(["myField"]); + + AssertProjection(projection, "{ '_ai' : 1, '_id' : 1, '_si' : 1, 'ai' : 1, 'cb' : 1, 'ct' : 1, 'dl' : 1, 'do.myField' : 1, 'id' : 1, 'is' : 1, 'mb' : 1, 'mt' : 1, 'ns' : 1, 'rf' : 1, 'sa' : 1, 'si' : 1, 'sj' : 1, 'ss' : 1, 'ts' : 1, 'vs' : 1 }"); + } + + [Fact] + public void Should_build_projection_without_included_field() + { + var projection = ExtensionSut.BuildProjection2(["myField.special", "myField"]); + + AssertProjection(projection, "{ '_ai' : 1, '_id' : 1, '_si' : 1, 'ai' : 1, 'cb' : 1, 'ct' : 1, 'dl' : 1, 'do.myField' : 1, 'id' : 1, 'is' : 1, 'mb' : 1, 'mt' : 1, 'ns' : 1, 'rf' : 1, 'sa' : 1, 'si' : 1, 'sj' : 1, 'ss' : 1, 'ts' : 1, 'vs' : 1 }"); + } + + private static void AssertProjection(ProjectionDefinition projection, string expected) + { + var rendered = + projection.Render( + BsonSerializer.SerializerRegistry.GetSerializer(), + BsonSerializer.SerializerRegistry) + .Document.ToString(); + + Assert.Equal(Cleanup(expected), rendered); + } + + private static string Cleanup(string filter) + { + return filter.Replace('\'', '"'); + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs index 4c2bd6cf3..b37162f0d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs @@ -46,7 +46,7 @@ public class ConvertDataTests : GivenContext { var content = CreateContent(); - await sut.EnrichAsync(FrontendContext, new[] { content }, SchemaProvider(), CancellationToken); + await sut.EnrichAsync(FrontendContext, [content], SchemaProvider(), CancellationToken); Assert.NotNull(content.Data); } @@ -82,7 +82,7 @@ public class ConvertDataTests : GivenContext JsonValue.Object() .Add("nested", JsonValue.Array("default3"))))); - await sut.EnrichAsync(ApiContext, new[] { content }, SchemaProvider(), CancellationToken); + await sut.EnrichAsync(ApiContext, [content], SchemaProvider(), CancellationToken); Assert.Equal(expected, content.Data); } @@ -116,12 +116,12 @@ public class ConvertDataTests : GivenContext .Add("nested", JsonValue.Array(id2))))); A.CallTo(() => assetRepository.QueryIdsAsync(AppId.Id, A>.That.Is(id1, id2), CancellationToken)) - .Returns(new List { id2 }); + .Returns([id2]); A.CallTo(() => contentRepository.QueryIdsAsync(App, A>.That.Is(id1, id2), SearchScope.All, CancellationToken)) - .Returns(new List { new ContentIdStatus(id2, id2, Status.Published) }); + .Returns([new ContentIdStatus(id2, id2, Status.Published)]); - await sut.EnrichAsync(ApiContext, new[] { content }, SchemaProvider(), CancellationToken); + await sut.EnrichAsync(ApiContext, [content], SchemaProvider(), CancellationToken); Assert.Equal(expected, content.Data); } @@ -155,12 +155,12 @@ public class ConvertDataTests : GivenContext .Add("nested", JsonValue.Array())))); A.CallTo(() => assetRepository.QueryIdsAsync(AppId.Id, A>.That.Is(id1, id2), CancellationToken)) - .Returns(new List()); + .Returns([]); A.CallTo(() => contentRepository.QueryIdsAsync(App, A>.That.Is(id1, id2), SearchScope.All, CancellationToken)) - .Returns(new List()); + .Returns([]); - await sut.EnrichAsync(ApiContext, new[] { content }, SchemaProvider(), CancellationToken); + await sut.EnrichAsync(ApiContext, [content], SchemaProvider(), CancellationToken); Assert.Equal(expected, content.Data); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs new file mode 100644 index 000000000..9647eab10 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Shared; + +namespace Squidex.Domain.Apps.Entities.Schemas; + +public class SchemasChatToolTests : GivenContext +{ + private readonly IUrlGenerator urlGenerator = A.Fake(); + private readonly SchemasChatTool sut; + + public SchemasChatToolTests() + { + sut = new SchemasChatTool(AppProvider, TestUtils.DefaultSerializer, urlGenerator); + } + + [Fact] + public async Task Should_return_schemas_if_user_has_permission() + { + var chatContext = new AppChatContext + { + BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppSchemasRead, App.Name).Id) + }; + + var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); + + Assert.NotNull(tool); + Assert.Equal("schemas", tool.Spec.Name); + Assert.Equal("Schemas", tool.Spec.DisplayName); + + var result = await tool.ExecuteAsync(null!, default); + + Assert.Contains(Schema.Name, result); + + A.CallTo(() => urlGenerator.SchemasUI(AppId)) + .MustHaveHappened(); + + A.CallTo(() => urlGenerator.SchemaUI(AppId, SchemaId)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_return_tools_if_user_no_permission() + { + var chatContext = new AppChatContext + { + BaseContext = FrontendContext + }; + + var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); + + Assert.Null(tool); + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index 58362d230..65dc8a797 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -27,7 +27,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -39,7 +39,7 @@ - + all diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/GivenContext.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/GivenContext.cs index 7be67ff2f..981889d22 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/GivenContext.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/GivenContext.cs @@ -160,6 +160,9 @@ public abstract class GivenContext A.CallTo(() => result.GetSchemaAsync(AppId.Id, SchemaId.Name, A._, A._)) .ReturnsLazily(() => Schema); + A.CallTo(() => result.GetSchemasAsync(AppId.Id, A._)) + .ReturnsLazily(() => [Schema]); + A.CallTo(() => result.GetAppWithSchemaAsync(AppId.Id, SchemaId.Id, A._, A._)) .ReturnsLazily(() => (App, Schema)); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddLanguage_should_create_events_and_add_language.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddLanguage_should_create_events_and_add_language.verified.txt index 648e61327..ac040795d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddLanguage_should_create_events_and_add_language.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddLanguage_should_create_events_and_add_language.verified.txt @@ -90,7 +90,7 @@ en, de ], - Languages: { + Values: { de: { IsOptional: false }, diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddRole_should_create_events_and_add_role.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddRole_should_create_events_and_add_role.verified.txt index fdf47c9e8..a5d20fd2f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddRole_should_create_events_and_add_role.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddRole_should_create_events_and_add_role.verified.txt @@ -100,7 +100,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddWorkflow_should_create_events_and_add_workflow.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddWorkflow_should_create_events_and_add_workflow.verified.txt index 39270bf75..7a7d5096c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddWorkflow_should_create_events_and_add_workflow.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AddWorkflow_should_create_events_and_add_workflow.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_events_and_add_contributor.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_events_and_add_contributor.verified.txt index a138da5f0..c3c4e65ae 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_events_and_add_contributor.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_events_and_add_contributor.verified.txt @@ -90,7 +90,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_update_events_and_update_contributor.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_update_events_and_update_contributor.verified.txt index 02362e129..8066bf298 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_update_events_and_update_contributor.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AssignContributor_should_create_update_events_and_update_contributor.verified.txt @@ -90,7 +90,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AttachClient_should_create_events_and_add_client.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AttachClient_should_create_events_and_add_client.verified.txt index 9c777c2cc..74e8c18a3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AttachClient_should_create_events_and_add_client.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.AttachClient_should_create_events_and_add_client.verified.txt @@ -96,7 +96,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_create_events_and_update_plan.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_create_events_and_update_plan.verified.txt index 194806f4f..1426eefd7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_create_events_and_update_plan.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_create_events_and_update_plan.verified.txt @@ -93,7 +93,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_reset_plan_for_free_plan.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_reset_plan_for_free_plan.verified.txt index 0c13668a2..6089f817d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_reset_plan_for_free_plan.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_from_callback_should_reset_plan_for_free_plan.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_create_events_and_update_plan.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_create_events_and_update_plan.verified.txt index 194806f4f..1426eefd7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_create_events_and_update_plan.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_create_events_and_update_plan.verified.txt @@ -93,7 +93,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_call_billing_manager_for_callback.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_call_billing_manager_for_callback.verified.txt index 194806f4f..1426eefd7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_call_billing_manager_for_callback.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_call_billing_manager_for_callback.verified.txt @@ -93,7 +93,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_make_update_for_redirect.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_make_update_for_redirect.verified.txt index 452724a82..9301c32f8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_make_update_for_redirect.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_not_make_update_for_redirect.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_reset_plan_for_free_plan.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_reset_plan_for_free_plan.verified.txt index 0c13668a2..6089f817d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_reset_plan_for_free_plan.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.ChangePlan_should_reset_plan_for_free_plan.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_create_events_and_set_intitial_state.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_create_events_and_set_intitial_state.verified.txt index d35344a35..e6b6b9472 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_create_events_and_set_intitial_state.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_create_events_and_set_intitial_state.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_not_assign_client_as_contributor.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_not_assign_client_as_contributor.verified.txt index de668c288..e0a1d371f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_not_assign_client_as_contributor.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Create_should_not_assign_client_as_contributor.verified.txt @@ -86,7 +86,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteApp_should_create_events_and_update_deleted_flag.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteApp_should_create_events_and_update_deleted_flag.verified.txt index 057224207..d16f8c810 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteApp_should_create_events_and_update_deleted_flag.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteApp_should_create_events_and_update_deleted_flag.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteRole_should_create_events_and_delete_role.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteRole_should_create_events_and_delete_role.verified.txt index 34c30cf74..d6305ce14 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteRole_should_create_events_and_delete_role.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteRole_should_create_events_and_delete_role.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteWorkflow_should_create_events_and_remove_workflow.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteWorkflow_should_create_events_and_remove_workflow.verified.txt index 38b5cbee4..37b40b78c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteWorkflow_should_create_events_and_remove_workflow.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.DeleteWorkflow_should_create_events_and_remove_workflow.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveContributor_should_create_events_and_remove_contributor.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveContributor_should_create_events_and_remove_contributor.verified.txt index 8445268be..b56580610 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveContributor_should_create_events_and_remove_contributor.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveContributor_should_create_events_and_remove_contributor.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveImage_should_create_events_and_update_image.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveImage_should_create_events_and_update_image.verified.txt index 0c13668a2..6089f817d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveImage_should_create_events_and_update_image.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveImage_should_create_events_and_update_image.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveLanguage_should_create_events_and_remove_language.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveLanguage_should_create_events_and_remove_language.verified.txt index 4b5137b05..6e6ebcceb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveLanguage_should_create_events_and_remove_language.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RemoveLanguage_should_create_events_and_remove_language.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RevokeClient_should_create_events_and_remove_client.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RevokeClient_should_create_events_and_remove_client.verified.txt index c0306559f..abba6bb5c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RevokeClient_should_create_events_and_remove_client.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.RevokeClient_should_create_events_and_remove_client.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_from_team_should_create_events_and_set_team.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_from_team_should_create_events_and_set_team.verified.txt index 0c13668a2..6089f817d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_from_team_should_create_events_and_set_team.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_from_team_should_create_events_and_set_team.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_should_create_events_and_set_team.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_should_create_events_and_set_team.verified.txt index f81e4cccf..10238f91e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_should_create_events_and_set_team.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Transfer_should_create_events_and_set_team.verified.txt @@ -90,7 +90,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateClient_should_create_events_and_update_client.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateClient_should_create_events_and_update_client.verified.txt index 7479a3cb4..275ae2e34 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateClient_should_create_events_and_update_client.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateClient_should_create_events_and_update_client.verified.txt @@ -96,7 +96,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateLanguage_should_create_events_and_update_language.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateLanguage_should_create_events_and_update_language.verified.txt index 10eee608d..4aa357034 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateLanguage_should_create_events_and_update_language.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateLanguage_should_create_events_and_update_language.verified.txt @@ -90,7 +90,7 @@ en, de ], - Languages: { + Values: { de: { IsOptional: false, Fallbacks: [ diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateRole_should_create_events_and_update_role.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateRole_should_create_events_and_update_role.verified.txt index c37998dcd..96894bbe4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateRole_should_create_events_and_update_role.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateRole_should_create_events_and_update_role.verified.txt @@ -110,7 +110,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateSettings_should_create_event_and_update_settings.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateSettings_should_create_event_and_update_settings.verified.txt index 2f4e72ad9..bf8386aaf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateSettings_should_create_event_and_update_settings.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateSettings_should_create_event_and_update_settings.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateWorkflow_should_create_events_and_update_workflow.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateWorkflow_should_create_events_and_update_workflow.verified.txt index 479c8c973..65a5ac74c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateWorkflow_should_create_events_and_update_workflow.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UpdateWorkflow_should_create_events_and_update_workflow.verified.txt @@ -89,7 +89,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Update_should_create_events_and_update_label_and_description.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Update_should_create_events_and_update_label_and_description.verified.txt index b815df3fb..227c534be 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Update_should_create_events_and_update_label_and_description.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.Update_should_create_events_and_update_label_and_description.verified.txt @@ -91,7 +91,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UploadImage_should_create_events_and_update_image.verified.txt b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UploadImage_should_create_events_and_update_image.verified.txt index 350ca448a..6d075d669 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UploadImage_should_create_events_and_update_image.verified.txt +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Verify/AppDomainObjectTests.UploadImage_should_create_events_and_update_image.verified.txt @@ -93,7 +93,7 @@ AllKeys: [ en ], - Languages: { + Values: { en: { IsOptional: false } diff --git a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj index b1c8015d0..c149ff362 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index ad18c519d..219f5c5c5 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj index 119224757..0cdfb499f 100644 --- a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj +++ b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj @@ -16,7 +16,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/frontend/src/app/framework/angular/forms/editors/date-time-editor.component.scss b/frontend/src/app/framework/angular/forms/editors/date-time-editor.component.scss index db1990278..df536875c 100644 --- a/frontend/src/app/framework/angular/forms/editors/date-time-editor.component.scss +++ b/frontend/src/app/framework/angular/forms/editors/date-time-editor.component.scss @@ -36,12 +36,12 @@ .btn { &-clear { @include absolute(auto, 4px, 3px, auto); - z-index: 100; + z-index: 1; } &-time-mode { @include absolute(1px, auto, -1px, auto); - z-index: 100; + z-index: 1; } &:focus { diff --git a/frontend/src/app/shell/pages/internal/chat-menu.component.html b/frontend/src/app/shell/pages/internal/chat-menu.component.html new file mode 100644 index 000000000..b4caee711 --- /dev/null +++ b/frontend/src/app/shell/pages/internal/chat-menu.component.html @@ -0,0 +1,9 @@ +@if ((appsState.selectedApp | async) && hasChatBot) { + +} + + diff --git a/frontend/src/app/shell/pages/internal/chat-menu.component.scss b/frontend/src/app/shell/pages/internal/chat-menu.component.scss new file mode 100644 index 000000000..c824ee2c3 --- /dev/null +++ b/frontend/src/app/shell/pages/internal/chat-menu.component.scss @@ -0,0 +1,7 @@ +@import 'mixins'; +@import 'vars'; + +.nav-link { + font-weight: 450; + font-size: 1.3rem; +} \ No newline at end of file diff --git a/frontend/src/app/shell/pages/internal/chat-menu.component.ts b/frontend/src/app/shell/pages/internal/chat-menu.component.ts new file mode 100644 index 000000000..562144d84 --- /dev/null +++ b/frontend/src/app/shell/pages/internal/chat-menu.component.ts @@ -0,0 +1,33 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { AppsState, ChatDialogComponent, DialogModel, ModalDirective, UIOptions } from '@app/shared'; + +@Component({ + standalone: true, + selector: 'sqx-chat-menu', + styleUrls: ['./chat-menu.component.scss'], + templateUrl: './chat-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AsyncPipe, + ChatDialogComponent, + ModalDirective + ], +}) +export class ChatMenuComponent { + public readonly chatDialog = new DialogModel(); + + public readonly hasChatBot = inject(UIOptions).value.canUseChatBot; + + constructor( + public readonly appsState: AppsState + ) { + } +} diff --git a/frontend/src/app/shell/pages/internal/internal-area.component.html b/frontend/src/app/shell/pages/internal/internal-area.component.html index f6db2f9ec..e9ce5450d 100644 --- a/frontend/src/app/shell/pages/internal/internal-area.component.html +++ b/frontend/src/app/shell/pages/internal/internal-area.component.html @@ -8,6 +8,7 @@