Browse Source

Chatbot support (#1001)

* Select client.

* Fixes to OpenAPI: Parameter order.

* Fix scripting.

* Fix content type.

* Save all files.

* Fix tests

* Improve errors.

* Fix schema name.

* Fix exception type.

* Fix versions.

* Fix script.

* Chatbot.

* Added missing files.

* Added a loading spinner.

* Fix migration

* Revert change.

* Fix login flow.

* Fix home page.

* Fix GraphQL.

* Add schema name.
pull/1003/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
ae96470356
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  2. 5
      backend/i18n/frontend_en.json
  3. 5
      backend/i18n/frontend_fr.json
  4. 5
      backend/i18n/frontend_it.json
  5. 5
      backend/i18n/frontend_nl.json
  6. 5
      backend/i18n/frontend_pt.json
  7. 5
      backend/i18n/frontend_zh.json
  8. 5
      backend/i18n/source/frontend_en.json
  9. 2
      backend/src/Migrations/Migrations.csproj
  10. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs
  11. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  12. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddSchemaNames.cs
  13. 535
      backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs
  14. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx
  15. 73
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  16. 114
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs
  17. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  18. 30
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs
  19. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  20. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/Tokenizer.cs
  21. 75
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  22. 38
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs
  23. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs
  24. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentInterfaceGraphType.cs
  25. 53
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs
  26. 25
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs
  27. 11
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs
  28. 44
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs
  29. 5
      backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs
  30. 8
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  31. 2
      backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  32. 4
      backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  33. 2
      backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs
  34. 8
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  35. 2
      backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs
  36. 4
      backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
  37. 6
      backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  38. 26
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  39. 2
      backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs
  40. 2
      backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs
  41. 2
      backend/src/Squidex.Shared/Squidex.Shared.csproj
  42. 3
      backend/src/Squidex.Web/Pipeline/CachingManager.cs
  43. 8
      backend/src/Squidex.Web/Squidex.Web.csproj
  44. 21
      backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs
  45. 10
      backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs
  46. 27
      backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
  47. 90
      backend/src/Squidex/Config/Domain/AssetServices.cs
  48. 20
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  49. 11
      backend/src/Squidex/Config/Domain/StoreServices.cs
  50. 48
      backend/src/Squidex/Squidex.csproj
  51. 13
      backend/src/Squidex/appsettings.json
  52. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs
  53. 150
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  54. 8
      backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  55. 25
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs
  56. 14
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  57. 8
      backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
  58. 4
      backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs
  59. 11
      backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  60. 8
      backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
  61. 1
      frontend/src/app/features/content/declarations.ts
  62. 3
      frontend/src/app/features/content/module.ts
  63. 8
      frontend/src/app/features/content/pages/content/editor/content-field.component.html
  64. 2
      frontend/src/app/features/content/pages/content/editor/content-field.component.scss
  65. 4
      frontend/src/app/features/content/pages/content/editor/field-languages.component.html
  66. 56
      frontend/src/app/features/content/shared/forms/chat-dialog.component.html
  67. 6
      frontend/src/app/features/content/shared/forms/chat-dialog.component.scss
  68. 65
      frontend/src/app/features/content/shared/forms/chat-dialog.component.ts
  69. 13
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  70. 10
      frontend/src/app/features/content/shared/forms/field-editor.component.ts
  71. 8
      frontend/src/app/features/settings/pages/templates/template.component.html
  72. 26
      frontend/src/app/shared/services/translations.service.spec.ts
  73. 12
      frontend/src/app/shared/services/translations.service.ts
  74. 12
      frontend/src/app/shell/pages/home/home-page.component.ts

18
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -14,27 +14,27 @@
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.0.0-beta.3" />
<PackageReference Include="Azure.Search.Documents" Version="11.4.0" />
<PackageReference Include="Confluent.Apache.Avro" Version="1.7.7.7" />
<PackageReference Include="Confluent.Kafka" Version="2.0.2" />
<PackageReference Include="Confluent.Kafka" Version="2.1.1" />
<PackageReference Include="Confluent.SchemaRegistry.Serdes" Version="1.3.0" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Elasticsearch.Net" Version="7.17.5" />
<PackageReference Include="Google.Cloud.Diagnostics.Common" Version="4.4.0" />
<PackageReference Include="Google.Cloud.Logging.V2" Version="3.6.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Azure.CognitiveServices.Vision.ComputerVision" Version="7.0.1" />
<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.21.2" />
<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.21.4" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.15.0" />
<PackageReference Include="NodaTime" Version="3.1.6" />
<PackageReference Include="Microsoft.OData.Core" Version="7.17.0" />
<PackageReference Include="NodaTime" Version="3.1.9" />
<PackageReference Include="OpenSearch.Net" Version="1.3.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.4.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.4.0" />
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.4.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.4.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.5.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.5.0" />
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.5.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.OpenTelemetry.Exporter.Stackdriver" Version="0.0.0-alpha.0.395" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

5
backend/i18n/frontend_en.json

@ -166,6 +166,11 @@
"backups.started": "Backup started, it can take several minutes to complete.",
"backups.startedLabel": "Started",
"backups.startFailed": "Failed to start backup.",
"chat.ask": "Ask",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"clients.add": "Add Client",
"clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client",

5
backend/i18n/frontend_fr.json

@ -166,6 +166,11 @@
"backups.started": "La sauvegarde a commencé, cela peut prendre plusieurs minutes.",
"backups.startedLabel": "Commencé",
"backups.startFailed": "Échec du démarrage de la sauvegarde.",
"chat.ask": "Ask",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"clients.add": "Ajouter un client",
"clients.add.description": "Ajoutez un client pour permettre à d'autres applications d'accéder à votre contenu.",
"clients.add.title": "Ajouter un nouveau client",

5
backend/i18n/frontend_it.json

@ -166,6 +166,11 @@
"backups.started": "Backup avviato, il suo completamento potrebbe richiedere alcuni minuti.",
"backups.startedLabel": "Avviato",
"backups.startFailed": "Non è stato possibile avviare il backup.",
"chat.ask": "Ask",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"clients.add": "Aggiungi un Client",
"clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client",

5
backend/i18n/frontend_nl.json

@ -166,6 +166,11 @@
"backups.started": "Back-up gestart, het kan enkele minuten duren om te voltooien.",
"backups.startedLabel": "Gestart",
"backups.startFailed": "Starten van back-up is mislukt.",
"chat.ask": "Ask",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"clients.add": "Client toevoegen",
"clients.add.description": "Voeg een client toe om andere applicaties toegang te geven tot uw inhoud.",
"clients.add.title": "Client toevoegen",

5
backend/i18n/frontend_pt.json

@ -166,6 +166,11 @@
"backups.started": "O reforço começou, pode levar vários minutos para ser concluído.",
"backups.startedLabel": "Começou",
"backups.startFailed": "Falhou em começar o backup.",
"chat.ask": "Ask",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"clients.add": "Adicionar Cliente",
"clients.add.description": "Adicione um cliente para dar a outras aplicações acesso ao seu conteúdo.",
"clients.add.title": "Adicionar um novo Cliente",

5
backend/i18n/frontend_zh.json

@ -166,6 +166,11 @@
"backups.started": "备份已开始,可能需要几分钟才能完成。",
"backups.startedLabel": "开始",
"backups.startFailed": "启动备份失败。",
"chat.ask": "Ask",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"clients.add": "添加客户端",
"clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client",

5
backend/i18n/source/frontend_en.json

@ -166,6 +166,11 @@
"backups.started": "Backup started, it can take several minutes to complete.",
"backups.startedLabel": "Started",
"backups.startFailed": "Failed to start backup.",
"chat.ask": "Ask",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"clients.add": "Add Client",
"clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client",

2
backend/src/Migrations/Migrations.csproj

@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs

@ -18,6 +18,8 @@ public sealed record Component(string Type, JsonObject Data, Schema Schema)
{
public const string Discriminator = "schemaId";
public const string Descriptor = "schemaName";
public string Type { get; } = Guard.NotNullOrEmpty(Type);
public Schema Schema { get; } = Guard.NotNull(Schema);

2
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -12,7 +12,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

4
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddSchemaNames.cs

@ -28,7 +28,7 @@ public sealed class AddSchemaNames : IContentItemConverter
return source;
}
if (source.ContainsKey("schemaName"))
if (source.ContainsKey(Component.Descriptor))
{
return source;
}
@ -42,7 +42,7 @@ public sealed class AddSchemaNames : IContentItemConverter
if (components.TryGetValue(id, out var component))
{
source["schemaName"] = component.Name;
source[Component.Descriptor] = component.Name;
}
return source;

535
backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs

@ -8,298 +8,317 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace Squidex.Domain.Apps.Core.Properties;
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
namespace Squidex.Domain.Apps.Core.Properties {
using System;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
set {
resourceCulture = value;
/// <summary>
/// Looks up a localized string similar to The download URL to the asset..
/// </summary>
internal static string ScriptingAssetContentAppUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentAppUrl", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to The download URL to the asset..
/// </summary>
internal static string ScriptingAssetContentAppUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentAppUrl", resourceCulture);
/// <summary>
/// Looks up a localized string similar to The download URL to the asset using the file slug instead of the ID..
/// </summary>
internal static string ScriptingAssetContentSlugUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentSlugUrl", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to The download URL to the asset using the file slug instead of the ID..
/// </summary>
internal static string ScriptingAssetContentSlugUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentSlugUrl", resourceCulture);
/// <summary>
/// Looks up a localized string similar to The download URL to the asset without the app name (deprecated)..
/// </summary>
internal static string ScriptingAssetContentUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentUrl", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to The download URL to the asset without the app name (deprecated)..
/// </summary>
internal static string ScriptingAssetContentUrl {
get {
return ResourceManager.GetString("ScriptingAssetContentUrl", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text..
/// </summary>
internal static string ScriptingCharacterCount {
get {
return ResourceManager.GetString("ScriptingCharacterCount", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text..
/// </summary>
internal static string ScriptingCharacterCount {
get {
return ResourceManager.GetString("ScriptingCharacterCount", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Completes the script when an async method is used..
/// </summary>
internal static string ScriptingComplete {
get {
return ResourceManager.GetString("ScriptingComplete", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Completes the script when an async method is used..
/// </summary>
internal static string ScriptingComplete {
get {
return ResourceManager.GetString("ScriptingComplete", resourceCulture);
/// <summary>
/// Looks up a localized string similar to The status of the content..
/// </summary>
internal static string ScriptingContentAction {
get {
return ResourceManager.GetString("ScriptingContentAction", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to The status of the content..
/// </summary>
internal static string ScriptingContentAction {
get {
return ResourceManager.GetString("ScriptingContentAction", resourceCulture);
/// <summary>
/// Looks up a localized string similar to The URL to the content in the UI..
/// </summary>
internal static string ScriptingContentUrl {
get {
return ResourceManager.GetString("ScriptingContentUrl", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to The URL to the content in the UI..
/// </summary>
internal static string ScriptingContentUrl {
get {
return ResourceManager.GetString("ScriptingContentUrl", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingDeleteJson {
get {
return ResourceManager.GetString("ScriptingDeleteJson", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingDeleteJson {
get {
return ResourceManager.GetString("ScriptingDeleteJson", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Tell Squidex to not allow the current operation and to return a 400 (BadRequest)..
/// </summary>
internal static string ScriptingDisallow {
get {
return ResourceManager.GetString("ScriptingDisallow", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Tell Squidex to not allow the current operation and to return a 400 (BadRequest)..
/// </summary>
internal static string ScriptingDisallow {
get {
return ResourceManager.GetString("ScriptingDisallow", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern..
/// </summary>
internal static string ScriptingFormatDate {
get {
return ResourceManager.GetString("ScriptingFormatDate", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern..
/// </summary>
internal static string ScriptingFormatDate {
get {
return ResourceManager.GetString("ScriptingFormatDate", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern..
/// </summary>
internal static string ScriptingFormatTime {
get {
return ResourceManager.GetString("ScriptingFormatTime", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern..
/// </summary>
internal static string ScriptingFormatTime {
get {
return ResourceManager.GetString("ScriptingFormatTime", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Uses OpenAI or other machine learning platforms to generate content from a prompt..
/// </summary>
internal static string ScriptingGenerate {
get {
return ResourceManager.GetString("ScriptingGenerate", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Makes a GET request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingGetJSON {
get {
return ResourceManager.GetString("ScriptingGetJSON", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Makes a GET request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingGetJSON {
get {
return ResourceManager.GetString("ScriptingGetJSON", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Generates a guid..
/// </summary>
internal static string ScriptingGuid {
get {
return ResourceManager.GetString("ScriptingGuid", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Generates a guid..
/// </summary>
internal static string ScriptingGuid {
get {
return ResourceManager.GetString("ScriptingGuid", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Converts a HTML string to plain text..
/// </summary>
internal static string ScriptingHtml2Text {
get {
return ResourceManager.GetString("ScriptingHtml2Text", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Converts a HTML string to plain text..
/// </summary>
internal static string ScriptingHtml2Text {
get {
return ResourceManager.GetString("ScriptingHtml2Text", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Converts a markdown string to plain text..
/// </summary>
internal static string ScriptingMarkdown2Text {
get {
return ResourceManager.GetString("ScriptingMarkdown2Text", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Converts a markdown string to plain text..
/// </summary>
internal static string ScriptingMarkdown2Text {
get {
return ResourceManager.GetString("ScriptingMarkdown2Text", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important..
/// </summary>
internal static string ScriptingMD5 {
get {
return ResourceManager.GetString("ScriptingMD5", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important..
/// </summary>
internal static string ScriptingMD5 {
get {
return ResourceManager.GetString("ScriptingMD5", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPatchJson {
get {
return ResourceManager.GetString("ScriptingPatchJson", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPatchJson {
get {
return ResourceManager.GetString("ScriptingPatchJson", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Makes a POST request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPostJSON {
get {
return ResourceManager.GetString("ScriptingPostJSON", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Makes a POST request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPostJSON {
get {
return ResourceManager.GetString("ScriptingPostJSON", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPutJson {
get {
return ResourceManager.GetString("ScriptingPutJson", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional..
/// </summary>
internal static string ScriptingPutJson {
get {
return ResourceManager.GetString("ScriptingPutJson", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Tell Squidex to reject the current operation and to return a 403 (Forbidden)..
/// </summary>
internal static string ScriptingReject {
get {
return ResourceManager.GetString("ScriptingReject", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Tell Squidex to reject the current operation and to return a 403 (Forbidden)..
/// </summary>
internal static string ScriptingReject {
get {
return ResourceManager.GetString("ScriptingReject", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Tell Squidex that you have modified the data and that the change should be applied..
/// </summary>
internal static string ScriptingReplace {
get {
return ResourceManager.GetString("ScriptingReplace", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Tell Squidex that you have modified the data and that the change should be applied..
/// </summary>
internal static string ScriptingReplace {
get {
return ResourceManager.GetString("ScriptingReplace", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords..
/// </summary>
internal static string ScriptingSHA256 {
get {
return ResourceManager.GetString("ScriptingSHA256", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords..
/// </summary>
internal static string ScriptingSHA256 {
get {
return ResourceManager.GetString("ScriptingSHA256", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords..
/// </summary>
internal static string ScriptingSHA512 {
get {
return ResourceManager.GetString("ScriptingSHA512", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords..
/// </summary>
internal static string ScriptingSHA512 {
get {
return ResourceManager.GetString("ScriptingSHA512", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs..
/// </summary>
internal static string ScriptingSlugify {
get {
return ResourceManager.GetString("ScriptingSlugify", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs..
/// </summary>
internal static string ScriptingSlugify {
get {
return ResourceManager.GetString("ScriptingSlugify", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Converts a text to camelCase..
/// </summary>
internal static string ScriptingToCamelCase {
get {
return ResourceManager.GetString("ScriptingToCamelCase", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Converts a text to camelCase..
/// </summary>
internal static string ScriptingToCamelCase {
get {
return ResourceManager.GetString("ScriptingToCamelCase", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Converts a text to PascalCase.
/// </summary>
internal static string ScriptingToPascalCase {
get {
return ResourceManager.GetString("ScriptingToPascalCase", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Converts a text to PascalCase.
/// </summary>
internal static string ScriptingToPascalCase {
get {
return ResourceManager.GetString("ScriptingToPascalCase", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Counts the number of words in a text. Useful in combination with html2Text or markdown2Text..
/// </summary>
internal static string ScriptingWordCount {
get {
return ResourceManager.GetString("ScriptingWordCount", resourceCulture);
/// <summary>
/// Looks up a localized string similar to Translate from the source text to the specified language..
/// </summary>
internal static string ScriptingTranslate {
get {
return ResourceManager.GetString("ScriptingTranslate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Counts the number of words in a text. Useful in combination with html2Text or markdown2Text..
/// </summary>
internal static string ScriptingWordCount {
get {
return ResourceManager.GetString("ScriptingWordCount", resourceCulture);
}
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx

@ -150,6 +150,9 @@
<data name="ScriptingFormatTime" xml:space="preserve">
<value>Formats a JavaScript date object using the specified pattern.</value>
</data>
<data name="ScriptingGenerate" xml:space="preserve">
<value>Uses OpenAI or other machine learning platforms to generate content from a prompt.</value>
</data>
<data name="ScriptingGetJSON" xml:space="preserve">
<value>Makes a GET request to the defined URL and parses the result as JSON. Headers are optional.</value>
</data>
@ -195,6 +198,9 @@
<data name="ScriptingToPascalCase" xml:space="preserve">
<value>Converts a text to PascalCase</value>
</data>
<data name="ScriptingTranslate" xml:space="preserve">
<value>Translate from the source text to the specified language.</value>
</data>
<data name="ScriptingWordCount" xml:space="preserve">
<value>Counts the number of words in a text. Useful in combination with html2Text or markdown2Text.</value>
</data>

73
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs

@ -17,8 +17,8 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions;
public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor
{
private delegate void HttpJson(string url, Action<JsValue> callback, JsValue? headers = null, bool ignoreError = false);
private delegate void HttpJsonWithBody(string url, JsValue post, Action<JsValue> callback, JsValue? headers = null, bool ignoreError = false);
private delegate void HttpJsonDelegate(string url, Action<JsValue> callback, JsValue? headers = null, bool ignoreError = false);
private delegate void HttpJsonWithBodyDelegate(string url, JsValue post, Action<JsValue> callback, JsValue? headers = null, bool ignoreError = false);
private readonly IHttpClientFactory httpClientFactory;
public HttpJintExtension(IHttpClientFactory httpClientFactory)
@ -35,32 +35,9 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor
AddMethod(context, HttpMethod.Get, "getJSON");
}
public void Describe(AddDescription describe, ScriptScope scope)
{
if (!scope.HasFlag(ScriptScope.Async))
{
return;
}
describe(JsonType.Function, "getJSON(url, callback, headers?, ignoreError?)",
Resources.ScriptingGetJSON);
describe(JsonType.Function, "postJSON(url, body, callback, headers?, ignoreError?)",
Resources.ScriptingPostJSON);
describe(JsonType.Function, "putJSON(url, body, callback, headers?, ignoreError?)",
Resources.ScriptingPutJson);
describe(JsonType.Function, "patchJSON(url, body, callback, headers?, ignoreError?)",
Resources.ScriptingPatchJson);
describe(JsonType.Function, "deleteJSON(url, callback, headers?, ignoreError?)",
Resources.ScriptingDeleteJson);
}
private void AddMethod(ScriptExecutionContext context, HttpMethod method, string name)
{
var action = new HttpJson((url, callback, headers, ignoreError) =>
var action = new HttpJsonDelegate((url, callback, headers, ignoreError) =>
{
Request(context, method, url, null, callback, headers, ignoreError);
});
@ -70,7 +47,7 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor
private void AddBodyMethod(ScriptExecutionContext context, HttpMethod method, string name)
{
var action = new HttpJsonWithBody((url, body, callback, headers, ignoreError) =>
var action = new HttpJsonWithBodyDelegate((url, body, callback, headers, ignoreError) =>
{
Request(context, method, url, body, callback, headers, ignoreError);
});
@ -80,21 +57,20 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor
private void Request(ScriptExecutionContext context, HttpMethod method, string url, JsValue? body, Action<JsValue> callback, JsValue? headers, bool ignoreError)
{
context.Schedule(async (scheduler, ct) =>
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
throw new JavaScriptException("URL is not valid.");
}
throw new JavaScriptException("URL is not valid.");
}
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
try
{
var httpClient = httpClientFactory.CreateClient("Jint");
var request = CreateRequest(context, method, uri, body, headers);
@ -186,4 +162,27 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor
return jsonValue;
}
public void Describe(AddDescription describe, ScriptScope scope)
{
if (!scope.HasFlag(ScriptScope.Async))
{
return;
}
describe(JsonType.Function, "getJSON(url, callback, headers?, ignoreError?)",
Resources.ScriptingGetJSON);
describe(JsonType.Function, "postJSON(url, body, callback, headers?, ignoreError?)",
Resources.ScriptingPostJSON);
describe(JsonType.Function, "putJSON(url, body, callback, headers?, ignoreError?)",
Resources.ScriptingPutJson);
describe(JsonType.Function, "patchJSON(url, body, callback, headers?, ignoreError?)",
Resources.ScriptingPatchJson);
describe(JsonType.Function, "deleteJSON(url, callback, headers?, ignoreError?)",
Resources.ScriptingDeleteJson);
}
}

114
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs

@ -0,0 +1,114 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint.Native;
using Jint.Runtime;
using Squidex.Domain.Apps.Core.Properties;
using Squidex.Text.ChatBots;
using Squidex.Text.Translations;
#pragma warning disable CA1826 // Do not use Enumerable methods on indexable collections
namespace Squidex.Domain.Apps.Core.Scripting.Extensions;
public sealed class StringAsyncJintExtension : IJintExtension, IScriptDescriptor
{
private delegate void TextGenerateDelegate(string prompt, Action<JsValue> callback);
private delegate void TextTranslateDelegate(string text, string language, Action<JsValue> callback, string sourceLanguage);
private readonly ITranslator translator;
private readonly IChatBot chatBot;
public StringAsyncJintExtension(ITranslator translator, IChatBot chatBot)
{
this.translator = translator;
this.chatBot = chatBot;
}
public void ExtendAsync(ScriptExecutionContext context)
{
var generate = new TextGenerateDelegate((prompt, callback) =>
{
Generate(context, prompt, callback);
});
var translate = new TextTranslateDelegate((text, language, callback, sourceLanguage) =>
{
Translate(context, text, language, callback, sourceLanguage);
});
context.Engine.SetValue("generate", generate);
context.Engine.SetValue("translate", translate);
}
private void Generate(ScriptExecutionContext context, string prompt, Action<JsValue> callback)
{
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
try
{
if (string.IsNullOrWhiteSpace(prompt))
{
scheduler.Run(callback, JsValue.Null);
return;
}
var choices = await chatBot.AskQuestionAsync(prompt, ct);
scheduler.Run(callback, JsValue.FromObject(context.Engine, choices.FirstOrDefault()));
}
catch (Exception ex)
{
throw new JavaScriptException(ex.Message);
}
});
}
private void Translate(ScriptExecutionContext context, string text, string language, Action<JsValue> callback, string sourceLanguage)
{
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
try
{
if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(language))
{
scheduler.Run(callback, JsValue.Null);
return;
}
var translation = await translator.TranslateAsync(text, language, sourceLanguage, ct);
scheduler.Run(callback, JsValue.FromObject(context.Engine, translation.Text));
}
catch (Exception ex)
{
throw new JavaScriptException(ex.Message);
}
});
}
public void Describe(AddDescription describe, ScriptScope scope)
{
if (scope.HasFlag(ScriptScope.Async))
{
describe(JsonType.Function, "generate(prompt, callback?",
Resources.ScriptingGenerate);
describe(JsonType.Function, "translate(text, language, callback, sourceLanguage?",
Resources.ScriptingTranslate);
}
}
}

4
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -21,14 +21,14 @@
<PackageReference Include="Fluid.Core" Version="2.4.0" />
<PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="Jint" Version="3.0.0-beta-2046" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="NJsonSchema" Version="10.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.5.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.9.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

30
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs

@ -20,33 +20,17 @@ public static class Adapt
public static IReadOnlyDictionary<string, string> PropertyMap
{
get
{
if (propertyMap == null)
{
propertyMap =
BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).AllMemberMaps
.ToDictionary(
x => x.MemberName,
x => x.ElementName,
StringComparer.OrdinalIgnoreCase);
}
return propertyMap;
}
get => propertyMap ??=
BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).AllMemberMaps
.ToDictionary(
x => x.MemberName,
x => x.ElementName,
StringComparer.OrdinalIgnoreCase);
}
public static IReadOnlyDictionary<string, PropertyPath> PathMap
{
get
{
if (pathMap == null)
{
pathMap = PropertyMap.ToDictionary(x => x.Key, x => (PropertyPath)x.Value);
}
return pathMap;
}
get => pathMap ??= PropertyMap.ToDictionary(x => x.Key, x => (PropertyPath)x.Value);
}
public static PropertyPath MapPath(PropertyPath path)

4
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -19,11 +19,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00015" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MongoDB.Driver" Version="2.19.0" />
<PackageReference Include="MongoDB.Driver" Version="2.20.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/Tokenizer.cs

@ -68,7 +68,7 @@ public static class Tokenizer
if (query.Length > 2 && query[2] == ':')
{
textLanguage = query.Substring(0, 2);
textLanguage = query[..2];
textReader.Read();
textReader.Read();
}

75
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs

@ -38,36 +38,12 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
public void ExtendAsync(ScriptExecutionContext context)
{
AddAssetText(context);
AddAssetBlurHash(context);
AddAsset(context);
AddGetAssetText(context);
AddGetAssetBlurHash(context);
AddGetAssetObject(context);
}
public void Describe(AddDescription describe, ScriptScope scope)
{
if (!scope.HasFlag(ScriptScope.Async))
{
return;
}
describe(JsonType.Function, "getAsset(ids, callback)",
Resources.ScriptingGetAsset,
deprecationReason: Resources.ScriptingGetAssetDeprecated);
describe(JsonType.Function, "getAssetV2(ids, callback)",
Resources.ScriptingGetAssetV2);
describe(JsonType.Function, "getAssets(ids, callback)",
Resources.ScriptingGetAssets);
describe(JsonType.Function, "getAssetText(asset, callback, encoding?)",
Resources.ScriptingGetAssetText);
describe(JsonType.Function, "getAssetBlurHash(asset, callback, x?, y?)",
Resources.ScriptingGetBlurHash);
}
private void AddAsset(ScriptExecutionContext context)
private void AddGetAssetObject(ScriptExecutionContext context)
{
if (!context.TryGetValue<DomainId>("appId", out var appId))
{
@ -94,7 +70,7 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
context.Engine.SetValue("getAssets", getAssets);
}
private void AddAssetText(ScriptExecutionContext context)
private void AddGetAssetText(ScriptExecutionContext context)
{
var action = new GetAssetTextDelegate((references, callback, encoding) =>
{
@ -104,7 +80,7 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
context.Engine.SetValue("getAssetText", action);
}
private void AddAssetBlurHash(ScriptExecutionContext context)
private void AddGetAssetBlurHash(ScriptExecutionContext context)
{
var getBlurHash = new GetBlurHashDelegate((input, callback, componentX, componentY) =>
{
@ -116,7 +92,10 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
private void GetText(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue? encoding)
{
Guard.NotNull(callback);
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
@ -166,7 +145,10 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
private void GetBlurHash(ScriptExecutionContext context, JsValue input, Action<JsValue> callback, JsValue? componentX, JsValue? componentY)
{
Guard.NotNull(callback);
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
@ -229,7 +211,10 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
private void GetAssets(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback);
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
@ -311,4 +296,28 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor
return app;
}
public void Describe(AddDescription describe, ScriptScope scope)
{
if (!scope.HasFlag(ScriptScope.Async))
{
return;
}
describe(JsonType.Function, "getAsset(ids, callback)",
Resources.ScriptingGetAsset,
deprecationReason: Resources.ScriptingGetAssetDeprecated);
describe(JsonType.Function, "getAssetV2(ids, callback)",
Resources.ScriptingGetAssetV2);
describe(JsonType.Function, "getAssets(ids, callback)",
Resources.ScriptingGetAssets);
describe(JsonType.Function, "getAssetText(asset, callback, encoding?)",
Resources.ScriptingGetAssetText);
describe(JsonType.Function, "getAssetBlurHash(asset, callback, x?, y?)",
Resources.ScriptingGetBlurHash);
}
}

38
backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs

@ -15,8 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter;
public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor
{
private delegate long CounterReset(string name, long value = 0);
private delegate void CounterResetV2(string name, Action<JsValue>? callback = null, long value = 0);
private delegate long CounterResetDelegate(string name, long value = 0);
private delegate void CounterResetV2Delegate(string name, Action<JsValue>? callback = null, long value = 0);
private readonly ICounterService counterService;
public CounterJintExtension(ICounterService counterService)
@ -38,7 +38,7 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor
context.Engine.SetValue("incrementCounter", increment);
var reset = new CounterReset((name, value) =>
var reset = new CounterResetDelegate((name, value) =>
{
return Reset(appId, name, value);
});
@ -60,7 +60,7 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor
context.Engine.SetValue("incrementCounterV2", increment);
var reset = new CounterResetV2((name, callback, value) =>
var reset = new CounterResetV2Delegate((name, callback, value) =>
{
ResetV2(context, appId, name, callback, value);
});
@ -68,21 +68,6 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor
context.Engine.SetValue("resetCounterV2", reset);
}
public void Describe(AddDescription describe, ScriptScope scope)
{
describe(JsonType.Function, "incrementCounter(name)",
Resources.ScriptingIncrementCounter);
describe(JsonType.Function, "incrementCounterV2(name, callback?)",
Resources.ScriptingIncrementCounterV2);
describe(JsonType.Function, "resetCounter(name, value?)",
Resources.ScriptingResetCounter);
describe(JsonType.Function, "resetCounter(name, callback?, value?)",
Resources.ScriptingResetCounterV2);
}
private long Increment(DomainId appId, string name)
{
return AsyncHelper.Sync(() => counterService.IncrementAsync(appId, name));
@ -118,4 +103,19 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor
}
});
}
public void Describe(AddDescription describe, ScriptScope scope)
{
describe(JsonType.Function, "incrementCounter(name)",
Resources.ScriptingIncrementCounter);
describe(JsonType.Function, "incrementCounterV2(name, callback?)",
Resources.ScriptingIncrementCounterV2);
describe(JsonType.Function, "resetCounter(name, value?)",
Resources.ScriptingResetCounter);
describe(JsonType.Function, "resetCounter(name, callback?, value?)",
Resources.ScriptingResetCounterV2);
}
}

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs

@ -27,6 +27,7 @@ internal sealed class ComponentGraphType : ObjectGraphType<JsonObject>
Description = $"The structure of the {schemaInfo.DisplayName} component schema.";
AddField(ContentFields.SchemaId);
AddField(ContentFields.SchemaName);
foreach (var fieldInfo in schemaInfo.Fields)
{

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentInterfaceGraphType.cs

@ -17,7 +17,8 @@ internal sealed class ComponentInterfaceGraphType : InterfaceGraphType<JsonObjec
// The name is used for equal comparison. Therefore it is important to treat it as readonly.
Name = "Component";
AddField(ContentFields.SchemaId);
AddField(ContentFields.SchemaIdNoResolver);
AddField(ContentFields.SchemaNameNoResolver);
Description = "The structure of all content types.";
}

53
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs

@ -7,6 +7,7 @@
using GraphQL.Resolvers;
using GraphQL.Types;
using Namotion.Reflection;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
@ -43,6 +44,8 @@ internal static class ContentFields
Description = FieldDescriptions.EntityId
};
public static readonly FieldType IdNoResolver = Id.WithouthResolver();
public static readonly FieldType Version = new FieldType
{
Name = "version",
@ -51,6 +54,8 @@ internal static class ContentFields
Description = FieldDescriptions.EntityVersion
};
public static readonly FieldType VersionNoResolver = Version.WithouthResolver();
public static readonly FieldType Created = new FieldType
{
Name = "created",
@ -59,6 +64,8 @@ internal static class ContentFields
Description = FieldDescriptions.EntityCreated
};
public static readonly FieldType CreatedNoResolver = Created.WithouthResolver();
public static readonly FieldType CreatedBy = new FieldType
{
Name = "createdBy",
@ -67,6 +74,8 @@ internal static class ContentFields
Description = FieldDescriptions.EntityCreatedBy
};
public static readonly FieldType CreatedByNoResolver = CreatedBy.WithouthResolver();
public static readonly FieldType CreatedByUser = new FieldType
{
Name = "createdByUser",
@ -75,6 +84,8 @@ internal static class ContentFields
Description = FieldDescriptions.EntityCreatedBy
};
public static readonly FieldType CreatedByUserNoResolver = CreatedByUser.WithouthResolver();
public static readonly FieldType LastModified = new FieldType
{
Name = "lastModified",
@ -83,6 +94,8 @@ internal static class ContentFields
Description = FieldDescriptions.EntityLastModified
};
public static readonly FieldType LastModifiedNoResolver = LastModified.WithouthResolver();
public static readonly FieldType LastModifiedBy = new FieldType
{
Name = "lastModifiedBy",
@ -91,6 +104,8 @@ internal static class ContentFields
Description = FieldDescriptions.EntityLastModifiedBy
};
public static readonly FieldType LastModifiedByNoResolver = LastModifiedBy.WithouthResolver();
public static readonly FieldType LastModifiedByUser = new FieldType
{
Name = "lastModifiedByUser",
@ -99,6 +114,8 @@ internal static class ContentFields
Description = FieldDescriptions.EntityLastModifiedBy
};
public static readonly FieldType LastModifiedByUserNoResolver = LastModifiedByUser.WithouthResolver();
public static readonly FieldType Status = new FieldType
{
Name = "status",
@ -107,6 +124,8 @@ internal static class ContentFields
Description = FieldDescriptions.ContentStatus
};
public static readonly FieldType StatusNoResolver = Status.WithouthResolver();
public static readonly FieldType StatusColor = new FieldType
{
Name = "statusColor",
@ -115,6 +134,8 @@ internal static class ContentFields
Description = FieldDescriptions.ContentStatusColor
};
public static readonly FieldType StatusColorNoResolver = StatusColor.WithouthResolver();
public static readonly FieldType NewStatus = new FieldType
{
Name = "newStatus",
@ -123,6 +144,8 @@ internal static class ContentFields
Description = FieldDescriptions.ContentNewStatus
};
public static readonly FieldType NewStatusNoResolver = NewStatus.WithouthResolver();
public static readonly FieldType NewStatusColor = new FieldType
{
Name = "newStatusColor",
@ -131,6 +154,8 @@ internal static class ContentFields
Description = FieldDescriptions.ContentStatusColor
};
public static readonly FieldType NewStatusColorNoResolver = NewStatusColor.WithouthResolver();
public static readonly FieldType SchemaId = new FieldType
{
Name = "schemaId",
@ -139,6 +164,18 @@ internal static class ContentFields
Description = FieldDescriptions.ContentSchemaId
};
public static readonly FieldType SchemaIdNoResolver = SchemaId.WithouthResolver();
public static readonly FieldType SchemaName = new FieldType
{
Name = "schemaName",
ResolvedType = Scalars.String,
Resolver = Resolve(x => GetSchemaName(x)),
Description = FieldDescriptions.ContentSchemaName
};
public static readonly FieldType SchemaNameNoResolver = SchemaName.WithouthResolver();
public static readonly FieldType Url = new FieldType
{
Name = "url",
@ -147,6 +184,8 @@ internal static class ContentFields
Description = FieldDescriptions.ContentUrl
};
public static readonly FieldType UrlNoResolver = Url.WithouthResolver();
public static readonly FieldType EditToken = new FieldType
{
Name = "editToken",
@ -155,6 +194,8 @@ internal static class ContentFields
Description = FieldDescriptions.EditToken
};
public static readonly FieldType EditTokenNoResolver = EditToken.WithouthResolver();
public static readonly FieldType DataDynamic = new FieldType
{
Name = "data__dynamic",
@ -163,6 +204,8 @@ internal static class ContentFields
Description = FieldDescriptions.ContentData
};
public static readonly FieldType DataDynamicNoResolver = DataDynamic.WithouthResolver();
public static readonly FieldType StringFieldText = new FieldType
{
Name = "text",
@ -188,4 +231,14 @@ internal static class ContentFields
{
return Resolvers.Sync(resolver);
}
private static string? GetSchemaName(JsonObject component)
{
if (component.TryGetValue("schemaName", out var name) && name.Type == JsonValueType.String)
{
return name.ToString();
}
return null;
}
}

25
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs

@ -16,18 +16,19 @@ internal sealed class ContentInterfaceGraphType : InterfaceGraphType<IEnrichedCo
// The name is used for equal comparison. Therefore it is important to treat it as readonly.
Name = "Content";
AddField(ContentFields.Id);
AddField(ContentFields.Version);
AddField(ContentFields.Created);
AddField(ContentFields.CreatedBy);
AddField(ContentFields.LastModified);
AddField(ContentFields.LastModifiedBy);
AddField(ContentFields.Status);
AddField(ContentFields.StatusColor);
AddField(ContentFields.NewStatus);
AddField(ContentFields.NewStatusColor);
AddField(ContentFields.DataDynamic);
AddField(ContentFields.Url);
AddField(ContentFields.IdNoResolver);
AddField(ContentFields.VersionNoResolver);
AddField(ContentFields.CreatedNoResolver);
AddField(ContentFields.CreatedByNoResolver);
AddField(ContentFields.LastModifiedNoResolver);
AddField(ContentFields.LastModifiedByNoResolver);
AddField(ContentFields.EditTokenNoResolver);
AddField(ContentFields.StatusNoResolver);
AddField(ContentFields.StatusColorNoResolver);
AddField(ContentFields.NewStatusNoResolver);
AddField(ContentFields.NewStatusColorNoResolver);
AddField(ContentFields.DataDynamicNoResolver);
AddField(ContentFields.UrlNoResolver);
Description = "The structure of all content types.";
}

11
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs

@ -17,6 +17,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
public static class SharedExtensions
{
internal static FieldType WithouthResolver(this FieldType source)
{
return new FieldType
{
Name = source.Name,
ResolvedType = source.ResolvedType,
Resolver = null,
Description = source.Name
};
}
internal static string BuildODataQuery(this IResolveFieldContext context)
{
var sb = DefaultPools.StringBuilder.Get();

44
backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs

@ -54,28 +54,13 @@ public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor
context.Engine.SetValue("getReferences", getReferences);
}
public void Describe(AddDescription describe, ScriptScope scope)
private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
if (!scope.HasFlag(ScriptScope.Async))
if (callback == null)
{
return;
throw new JavaScriptException("Callback is not defined.");
}
describe(JsonType.Function, "getReference(id, callback)",
Resources.ScriptingGetReference,
deprecationReason: Resources.ScriptingGetReferenceDeprecated);
describe(JsonType.Function, "getReferenceV2(id, callback)",
Resources.ScriptingGetReferenceV2);
describe(JsonType.Function, "getReferences(ids, callback)",
Resources.ScriptingGetReferences);
}
private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback);
context.Schedule(async (scheduler, ct) =>
{
var ids = references.ToIds();
@ -110,7 +95,10 @@ public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor
private void GetReference(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback);
if (callback == null)
{
throw new JavaScriptException("Callback is not defined.");
}
context.Schedule(async (scheduler, ct) =>
{
@ -157,4 +145,22 @@ public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor
return app;
}
public void Describe(AddDescription describe, ScriptScope scope)
{
if (!scope.HasFlag(ScriptScope.Async))
{
return;
}
describe(JsonType.Function, "getReference(id, callback)",
Resources.ScriptingGetReference,
deprecationReason: Resources.ScriptingGetReferenceDeprecated);
describe(JsonType.Function, "getReferenceV2(id, callback)",
Resources.ScriptingGetReferenceV2);
describe(JsonType.Function, "getReferences(ids, callback)",
Resources.ScriptingGetReferences);
}
}

5
backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs

@ -14,6 +14,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Email;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
using System.Globalization;
namespace Squidex.Domain.Apps.Entities.Notifications;
@ -187,12 +188,12 @@ public sealed class EmailUserNotifications : IUserNotifications
if (vars.ApiCallsLimit != null)
{
text = text.Replace("$API_CALLS_LIMIT", vars.ApiCallsLimit.ToString(), StringComparison.Ordinal);
text = text.Replace("$API_CALLS_LIMIT", vars.ApiCallsLimit.Value.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal);
}
if (vars.ApiCalls != null)
{
text = text.Replace("$API_CALLS", vars.ApiCalls.ToString(), StringComparison.Ordinal);
text = text.Replace("$API_CALLS", vars.ApiCalls.Value.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal);
}
text = text.Replace("$UI_URL", vars.URL, StringComparison.Ordinal);

8
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -20,16 +20,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="GraphQL" Version="7.3.0" />
<PackageReference Include="GraphQL.DataLoader" Version="7.3.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="GraphQL" Version="7.5.0" />
<PackageReference Include="GraphQL.DataLoader" Version="7.5.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Notifo.SDK" Version="1.7.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.CLI.Core" Version="10.3.0" />
<PackageReference Include="Squidex.CLI.Core" Version="11.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

2
backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj

@ -14,7 +14,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

4
backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -19,12 +19,12 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.19.0" />
<PackageReference Include="MongoDB.Driver" Version="2.20.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />

2
backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs

@ -50,7 +50,9 @@ public sealed class DefaultXmlRepository : IXmlRepository
{
var state = new State(element);
#pragma warning disable MA0134 // Observe result of async calls
store.WriteAsync(new SnapshotWriteJob<State>(DomainId.Create(friendlyName), state, 0));
#pragma warning restore MA0134 // Observe result of async calls
}
private async Task<IReadOnlyCollection<XElement>> GetAllElementsAsync()

8
backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -17,14 +17,14 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityModel" Version="6.0.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="IdentityModel" Version="6.1.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="7.0.8" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="4.1.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="4.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SharpPwned.NET" Version="2.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

2
backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs

@ -25,6 +25,7 @@ internal sealed class GetEventStoreSubscription : IEventSubscription
string? prefix,
string? streamFilter)
{
#pragma warning disable MA0134 // Observe result of async calls
Task.Run(async () =>
{
var ct = cts.Token;
@ -69,6 +70,7 @@ internal sealed class GetEventStoreSubscription : IEventSubscription
cancellationToken: ct);
}
}, cts.Token);
#pragma warning restore MA0134 // Observe result of async calls
}
public void Dispose()

4
backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj

@ -14,8 +14,8 @@
<PackageReference Include="EventStore.Client.Grpc.PersistentSubscriptions" Version="23.0.0" />
<PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="23.0.0" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.0.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.52.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Grpc.Net.Client" Version="2.54.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

6
backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -14,12 +14,12 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MongoDB.Driver" Version="2.19.0" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.19.0" />
<PackageReference Include="MongoDB.Driver" Version="2.20.0" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.20.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />

26
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -11,30 +11,30 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="3.6.0" />
<PackageReference Include="MailKit" Version="4.1.0" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="7.0.8" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.15.0" />
<PackageReference Include="NodaTime" Version="3.1.6" />
<PackageReference Include="OpenTelemetry.Api" Version="1.4.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.17.0" />
<PackageReference Include="NodaTime" Version="3.1.9" />
<PackageReference Include="OpenTelemetry.Api" Version="1.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="5.5.0" />
<PackageReference Include="Squidex.Caching" Version="5.5.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="5.5.0" />
<PackageReference Include="Squidex.Log" Version="5.5.0" />
<PackageReference Include="Squidex.Messaging" Version="5.5.0" />
<PackageReference Include="Squidex.Text" Version="5.5.0" />
<PackageReference Include="Squidex.Assets" Version="5.9.0" />
<PackageReference Include="Squidex.Caching" Version="5.9.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="5.9.0" />
<PackageReference Include="Squidex.Log" Version="5.9.0" />
<PackageReference Include="Squidex.Messaging" Version="5.9.0" />
<PackageReference Include="Squidex.Text" Version="5.9.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />

2
backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs

@ -54,6 +54,7 @@ public static class AsyncHelper
public static void Batch<TIn>(this Channel<object> source, Channel<object> target, int batchSize, int timeout,
CancellationToken ct = default)
{
#pragma warning disable MA0134 // Observe result of async calls
Task.Run(async () =>
{
var batch = new List<TIn>(batchSize);
@ -94,5 +95,6 @@ public static class AsyncHelper
await TrySendAsync();
}, ct).ContinueWith(x => target.Writer.TryComplete(x.Exception));
#pragma warning restore MA0134 // Observe result of async calls
}
}

2
backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs

@ -23,12 +23,14 @@ public static class TaskExtensions
}
else
{
#pragma warning disable MA0134 // Observe result of async calls
task.ContinueWith(
IgnoreTaskContinuation,
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted |
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
#pragma warning restore MA0134 // Observe result of async calls
}
}

2
backend/src/Squidex.Shared/Squidex.Shared.csproj

@ -9,7 +9,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

3
backend/src/Squidex.Web/Pipeline/CachingManager.cs

@ -16,6 +16,8 @@ using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
#pragma warning disable CA1822 // Mark members as static
namespace Squidex.Web.Pipeline;
public sealed class CachingManager : IRequestCache
@ -197,7 +199,6 @@ public sealed class CachingManager : IRequestCache
public CachingManager(IHttpContextAccessor httpContextAccessor, IOptions<CachingOptions> cachingOptions)
{
this.httpContextAccessor = httpContextAccessor;
this.cachingOptions = cachingOptions.Value;
stringBuilderPool = new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy

8
backend/src/Squidex.Web/Squidex.Web.csproj

@ -13,10 +13,10 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GraphQL" Version="7.3.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.3.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="7.3.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="GraphQL" Version="7.5.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.5.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="7.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

21
backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Validation;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Translations.Models;
[OpenApiRequest]
public sealed class AskDto
{
/// <summary>
/// The text to ask.
/// </summary>
[LocalizedRequired]
public string Prompt { get; set; }
}

10
backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs

@ -15,7 +15,13 @@ public sealed class TranslationDto
/// <summary>
/// The result of the translation.
/// </summary>
public TranslationResultCode Result { get; set; }
public TranslationStatus Status { get; set; }
/// <summary>
/// The result of the translation.
/// </summary>
[Obsolete("Use Status property now.")]
public TranslationStatus Result => Status;
/// <summary>
/// The translated text.
@ -24,6 +30,6 @@ public sealed class TranslationDto
public static TranslationDto FromDomain(TranslationResult translation)
{
return SimpleMapper.Map(translation, new TranslationDto { Result = translation.Code });
return SimpleMapper.Map(translation, new TranslationDto());
}
}

27
backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Translations.Models;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Text.ChatBots;
using Squidex.Text.Translations;
using Squidex.Web;
@ -17,15 +18,18 @@ namespace Squidex.Areas.Api.Controllers.Translations;
/// <summary>
/// Manage translations.
/// </summary>
[ApiModelValidation(true)]
[ApiExplorerSettings(GroupName = nameof(Translations))]
public sealed class TranslationsController : ApiController
{
private readonly ITranslator translator;
private readonly IChatBot chatBot;
public TranslationsController(ICommandBus commandBus, ITranslator translator)
public TranslationsController(ICommandBus commandBus, ITranslator translator, IChatBot chatBot)
: base(commandBus)
{
this.translator = translator;
this.chatBot = chatBot;
}
/// <summary>
@ -38,7 +42,7 @@ public sealed class TranslationsController : ApiController
[Route("apps/{app}/translations/")]
[ProducesResponseType(typeof(TranslationDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppTranslate)]
[ApiCosts(0)]
[ApiCosts(10)]
public async Task<IActionResult> PostTranslation(string app, [FromBody] TranslateDto request)
{
var result = await translator.TranslateAsync(request.Text, request.TargetLanguage, request.SourceLanguage, HttpContext.RequestAborted);
@ -46,4 +50,23 @@ public sealed class TranslationsController : ApiController
return Ok(response);
}
/// <summary>
/// Asks the chatbot a question a text.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The question request.</param>
/// <response code="200">Question asked.</response>.
[HttpPost]
[Route("apps/{app}/ask/")]
[ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppTranslate)]
[ApiCosts(10)]
public async Task<IActionResult> PostQuestion(string app, [FromBody] AskDto request)
{
var result = await chatBot.AskQuestionAsync(request.Prompt, HttpContext.RequestAborted);
var response = result;
return Ok(response);
}
}

90
backend/src/Squidex/Config/Domain/AssetServices.cs

@ -49,15 +49,6 @@ public static class AssetServices
services.AddTransientAs<AssetCache>()
.As<IAssetCache>();
services.AddSingletonAs<AssetTusRunner>()
.AsSelf();
services.AddSingletonAs<AssetTusStore>()
.As<ITusStore>().As<ITusExpirationStore>();
services.AddSingletonAs(c => InMemoryFileLockProvider.Instance)
.As<ITusFileLockProvider>();
services.AddSingletonAs<RebuildFiles>()
.AsSelf();
@ -108,6 +99,8 @@ public static class AssetServices
services.AddSingletonAs<SvgAssetMetadataSource>()
.As<IAssetMetadataSource>();
services.AddAssetTus();
}
public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config)
@ -116,43 +109,27 @@ public static class AssetServices
{
["Default"] = () =>
{
services.AddSingletonAs<NoopAssetStore>()
.AsOptional<IAssetStore>();
services.AddFolderAssetStore(config);
},
["Folder"] = () =>
{
var path = config.GetRequiredValue("assetStore:folder:path");
services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService<ILogger<FolderAssetStore>>()))
.As<IAssetStore>();
services.AddFolderAssetStore(config);
},
["GoogleCloud"] = () =>
{
var options = new GoogleCloudAssetOptions
{
BucketName = config.GetRequiredValue("assetStore:googleCloud:bucket")
};
services.AddSingletonAs(c => new GoogleCloudAssetStore(options))
.As<IAssetStore>();
services.AddGoogleCloudAssetStore(config);
},
["AzureBlob"] = () =>
{
var options = new AzureBlobAssetOptions
{
ConnectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"),
ContainerName = config.GetRequiredValue("assetStore:azureBlob:containerName")
};
services.AddSingletonAs(c => new AzureBlobAssetStore(options))
.As<IAssetStore>();
services.AddAzureBlobAssetStore(config);
},
["AmazonS3"] = () =>
{
var amazonS3Options = config.GetSection("assetStore:amazonS3").Get<AmazonS3AssetOptions>() ?? new ();
services.AddSingletonAs(c => new AmazonS3AssetStore(amazonS3Options))
.As<IAssetStore>();
services.AddAmazonS3AssetStore(config);
},
["FTP"] = () =>
{
services.AddFTPAssetStore(config);
},
["MongoDb"] = () =>
{
@ -160,48 +137,17 @@ public static class AssetServices
var mongoDatabaseName = config.GetRequiredValue("assetStore:mongoDb:database");
var mongoGridFsBucketName = config.GetRequiredValue("assetStore:mongoDb:bucket");
services.AddSingletonAs(c =>
{
var mongoClient = StoreServices.GetMongoClient(mongoConfiguration);
var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName);
var gridFsbucket = new GridFSBucket<string>(mongoDatabase, new GridFSBucketOptions
{
BucketName = mongoGridFsBucketName
});
return new MongoGridFsAssetStore(gridFsbucket);
})
.As<IAssetStore>();
},
["Ftp"] = () =>
{
var serverHost = config.GetRequiredValue("assetStore:ftp:serverHost");
var serverPort = config.GetOptionalValue<int>("assetStore:ftp:serverPort", 21);
var username = config.GetRequiredValue("assetStore:ftp:username");
var password = config.GetRequiredValue("assetStore:ftp:password");
var options = new FTPAssetOptions
services.AddMongoAssetStore(c =>
{
Path = config.GetOptionalValue("assetStore:ftp:path", "/")!
};
var mongoClient = StoreServices.GetMongoClient(mongoConfiguration);
var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName);
services.AddSingletonAs(c =>
return new GridFSBucket<string>(mongoDatabase, new GridFSBucketOptions
{
var factory = new Func<AsyncFtpClient>(() => new AsyncFtpClient(serverHost, username, password, serverPort));
return new FTPAssetStore(factory, options, c.GetRequiredService<ILogger<FTPAssetStore>>());
})
.As<IAssetStore>();
BucketName = mongoGridFsBucketName
});
});
}
});
services.AddSingletonAs<IInitializable>(c =>
{
var service = c.GetRequiredService<IAssetStore>();
return new DelegateInitializer(service.GetType().Name, service.InitializeAsync);
});
}
}

20
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -86,6 +86,9 @@ public static class InfrastructureServices
services.AddSingletonAs<StringWordsJintExtension>()
.As<IJintExtension>().As<IScriptDescriptor>();
services.AddSingletonAs<StringAsyncJintExtension>()
.As<IJintExtension>().As<IScriptDescriptor>();
services.AddSingletonAs<HttpJintExtension>()
.As<IJintExtension>().As<IScriptDescriptor>();
@ -127,26 +130,15 @@ public static class InfrastructureServices
public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config)
{
services.Configure<GoogleCloudTranslationOptions>(config,
"translations:googleCloud");
services.Configure<DeepLOptions>(config,
"translations:deepL");
services.Configure<LanguagesOptions>(config,
"languages");
services.AddSingletonAs<LanguagesInitializer>()
.AsSelf();
services.AddSingletonAs(c => new DeepLTranslationService(c.GetRequiredService<IOptions<DeepLOptions>>().Value))
.As<ITranslationService>();
services.AddSingletonAs(c => new GoogleCloudTranslationService(c.GetRequiredService<IOptions<GoogleCloudTranslationOptions>>().Value))
.As<ITranslationService>();
services.AddSingletonAs<Translator>()
.As<ITranslator>();
services.AddDeepLTranslations(config);
services.AddGoogleCloudTranslations(config);
services.AddOpenAIChatBot(config);
}
public static void AddSquidexLocalization(this IServiceCollection services)

11
backend/src/Squidex/Config/Domain/StoreServices.cs

@ -67,6 +67,7 @@ public static class StoreServices
var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database")!;
var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName)!;
services.AddMongoAssetKeyValueStore();
services.AddSingleton(typeof(ISnapshotStore<>), typeof(MongoSnapshotStore<>));
services.AddSingletonAs(c => GetMongoClient(mongoConfiguration))
@ -111,9 +112,6 @@ public static class StoreServices
services.AddHealthChecks()
.AddCheck<MongoHealthCheck>("MongoDB", tags: new[] { "node" });
services.AddSingletonAs<MongoAssetKeyValueStore<TusMetadata>>()
.As<IAssetKeyValueStore<TusMetadata>>();
services.AddSingletonAs<MongoRequestLogRepository>()
.As<IRequestLogRepository>();
@ -208,13 +206,6 @@ public static class StoreServices
services.AddSingleton(typeof(IPersistenceFactory<>),
typeof(Store<>));
services.AddSingletonAs<IInitializable>(c =>
{
var service = c.GetRequiredService<IAssetKeyValueStore<TusMetadata>>();
return new DelegateInitializer(service.GetType().Name, service.InitializeAsync);
});
}
public static IMongoClient GetMongoClient(string configuration)

48
backend/src/Squidex/Squidex.csproj

@ -34,24 +34,24 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="7.0.0" />
<PackageReference Include="GraphQL" Version="7.3.0" />
<PackageReference Include="GraphQL.MicrosoftDI" Version="7.3.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.3.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="7.0.2" />
<PackageReference Include="GraphQL" Version="7.5.0" />
<PackageReference Include="GraphQL.MicrosoftDI" Version="7.5.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.5.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="7.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="7.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.8" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.8" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.RulesetToEditorconfigConverter" Version="3.3.3" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.5" />
<PackageReference Include="Microsoft.OData.Core" Version="7.15.0" />
<PackageReference Include="MongoDB.Driver" Version="2.19.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.17.0" />
<PackageReference Include="MongoDB.Driver" Version="2.20.0" />
<PackageReference Include="MongoDB.Driver.Core.Extensions.OpenTelemetry" Version="1.0.0" />
<PackageReference Include="NetTopologySuite.IO.GeoJSON4STJ" Version="3.0.0" />
<PackageReference Include="NJsonSchema" Version="10.9.0" />
@ -61,19 +61,19 @@
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc7" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.0.0-rc7" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.1.19" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="5.5.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="5.5.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="5.5.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="5.5.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="5.5.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="5.5.0" />
<PackageReference Include="Squidex.Assets.S3" Version="5.5.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="5.5.0" />
<PackageReference Include="ReportGenerator" Version="5.1.22" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.S3" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="5.9.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="16.0.0" />
<PackageReference Include="Squidex.Hosting" Version="5.5.0" />
<PackageReference Include="Squidex.Messaging.All" Version="5.5.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.5.0" />
<PackageReference Include="Squidex.Hosting" Version="5.9.0" />
<PackageReference Include="Squidex.Messaging.All" Version="5.9.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.9.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="4.0.1-dev" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup>

13
backend/src/Squidex/appsettings.json

@ -550,11 +550,11 @@
"suppressXFrameOptionsHeader": false,
// Initial admin user.
"adminEmail": "",
"adminPassword": "",
"adminEmail": "hello@squidex.io",
"adminPassword": "1q2w3e$R",
// Recreate the admin if it does not exist or the password does not match.
"adminRecreate": false,
"adminRecreate": true,
// Client with all admin permissions.
"adminClientId": "",
@ -624,6 +624,13 @@
}
},
"chatbot": {
"openai": {
// The OpenAI API Key.
"apiKey": ""
}
},
"rebuild": {
// Set to true to rebuild apps.
"apps": false,

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs

@ -17,8 +17,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent;
public class ContentConversionTests
{
private readonly Schema schema;
private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE);
private readonly Language language = Language.DE;
private readonly ResolvedComponents components;
public ContentConversionTests()

150
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs

@ -7,6 +7,7 @@
using System.Net;
using System.Text;
using Jint.Runtime;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Scripting;
@ -15,12 +16,16 @@ using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
using Squidex.Text.ChatBots;
using Squidex.Text.Translations;
namespace Squidex.Domain.Apps.Core.Operations.Scripting;
public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
{
private readonly IHttpClientFactory httpClientFactory = A.Fake<IHttpClientFactory>();
private readonly ITranslator translator = A.Fake<ITranslator>();
private readonly IChatBot chatBot = A.Fake<IChatBot>();
private readonly JintScriptEngine sut;
public JintScriptEngineHelperTests()
@ -30,7 +35,8 @@ public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
new DateTimeJintExtension(),
new HttpJintExtension(httpClientFactory),
new StringJintExtension(),
new StringWordsJintExtension()
new StringWordsJintExtension(),
new StringAsyncJintExtension(translator, chatBot)
};
sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())),
@ -611,6 +617,148 @@ public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
Assert.Equal(expectedResult, actual);
}
[Fact]
public async Task Should_generate_content()
{
A.CallTo(() => chatBot.AskQuestionAsync("prompt", A<CancellationToken>._))
.Returns(new List<string> { "Generated" });
var vars = new ScriptVars
{
};
const string script = @"
generate('prompt', function(actual) {
complete(actual);
});
";
var actual = await sut.ExecuteAsync(vars, script);
Assert.Equal("Generated", actual.ToString());
}
[Theory]
[InlineData("null")]
[InlineData("''")]
[InlineData("' '")]
public async Task Should_return_null_string_on_generate_if_prompt_is_invalid(string input)
{
var vars = new ScriptVars
{
};
var script = $@"
generate({input}, function(actual) {{
complete(actual);
}});
";
var actual = await sut.ExecuteAsync(vars, script);
Assert.Equal(JsonValue.Null, actual);
A.CallTo(() => chatBot.AskQuestionAsync(A<string>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_throw_exception_on_generate_if_callback_is_null()
{
var vars = new ScriptVars
{
};
const string script = @"
generate('prompt', null);
";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script));
}
[Fact]
public async Task Should_translate_content()
{
A.CallTo(() => translator.TranslateAsync("text", "en", "it", A<CancellationToken>._))
.Returns(TranslationResult.Success("Translated", "it"));
var vars = new ScriptVars
{
};
const string script = @"
translate('text', 'en', function(actual) {
complete(actual);
}, 'it');
";
var actual = await sut.ExecuteAsync(vars, script);
Assert.Equal("Translated", actual.ToString());
}
[Theory]
[InlineData("null")]
[InlineData("''")]
[InlineData("' '")]
public async Task Should_return_null_string_on_translate_if_input_is_invalid(string input)
{
var vars = new ScriptVars
{
};
var script = $@"
translate({input}, 'en', function(actual) {{
complete(actual);
}});
";
var actual = await sut.ExecuteAsync(vars, script);
Assert.Equal(JsonValue.Null, actual);
A.CallTo(() => translator.TranslateAsync(A<string>._, A<string>._, A<string>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
[Theory]
[InlineData("null")]
[InlineData("''")]
[InlineData("' '")]
public async Task Should_return_null_string_on_input_if_target_language_is_invalid(string input)
{
var vars = new ScriptVars
{
};
var script = $@"
translate('text', {input}, function(actual) {{
complete(actual);
}});
";
var actual = await sut.ExecuteAsync(vars, script);
Assert.Equal(JsonValue.Null, actual);
A.CallTo(() => translator.TranslateAsync(A<string>._, A<string>._, A<string>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_throw_exception_on_translate_if_callback_is_null()
{
var vars = new ScriptVars
{
};
const string script = @"
translate('text', 'en', null);
";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script));
}
private MockupHttpHandler SetupRequest(HttpStatusCode statusCode = HttpStatusCode.OK)
{
var httpResponse = new HttpResponseMessage(statusCode)

8
backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -14,15 +14,15 @@
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.1" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="FakeItEasy" Version="7.4.0" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="NetTopologySuite.IO.GeoJSON4STJ" Version="3.0.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

25
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs

@ -7,6 +7,7 @@
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -87,6 +88,7 @@ public static class TestContent
myComponent {
iv {
schemaId
schemaName
schemaRef1Field
}
}
@ -98,10 +100,12 @@ public static class TestContent
__typename
... on MyRefSchema1Component {
schemaId
schemaName
schemaRef1Field
}
... on MyRefSchema2Component {
schemaId
schemaName
schemaRef2Field
}
}
@ -172,6 +176,7 @@ public static class TestContent
myComponent__Dynamic
myComponent {
schemaId
schemaName
schemaRef1Field
}
myComponents__Dynamic
@ -179,10 +184,12 @@ public static class TestContent
__typename
... on MyRefSchema1Component {
schemaId
schemaName
schemaRef1Field
}
... on MyRefSchema2Component {
schemaId
schemaName
schemaRef2Field
}
}
@ -244,6 +251,7 @@ public static class TestContent
.AddInvariant(
JsonValue.Object()
.Add(Component.Discriminator, TestSchemas.Ref1.Id)
.Add(Component.Descriptor, TestSchemas.Ref1.SchemaDef.Name)
.Add("schemaRef1Field", "Component1")))
.AddField("my-components",
new ContentFieldData()
@ -251,9 +259,11 @@ public static class TestContent
JsonValue.Array(
JsonValue.Object()
.Add(Component.Discriminator, TestSchemas.Ref1.Id)
.Add(Component.Descriptor, TestSchemas.Ref1.SchemaDef.Name)
.Add("schemaRef1Field", "Component1"),
JsonValue.Object()
.Add(Component.Discriminator, TestSchemas.Ref2.Id)
.Add(Component.Descriptor, TestSchemas.Ref2.SchemaDef.Name)
.Add("schemaRef2Field", "Component2"))))
.AddField("my-json",
new ContentFieldData()
@ -484,6 +494,7 @@ public static class TestContent
iv = new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref1.Id.ToString(),
["schemaName"] = TestSchemas.Ref1.SchemaDef.Name,
["schemaRef1Field"] = "Component1"
}
},
@ -494,11 +505,13 @@ public static class TestContent
new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref1.Id.ToString(),
["schemaName"] = TestSchemas.Ref1.SchemaDef.Name,
["schemaRef1Field"] = "Component1"
},
new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref2.Id.ToString(),
["schemaName"] = TestSchemas.Ref2.SchemaDef.Name,
["schemaRef2Field"] = "Component2"
}
}
@ -648,6 +661,7 @@ public static class TestContent
iv = new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref1.Id.ToString(),
["schemaName"] = TestSchemas.Ref1.SchemaDef.Name,
["schemaRef1Field"] = "Component1"
}
},
@ -656,6 +670,7 @@ public static class TestContent
iv = new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref1.Id.ToString(),
["schemaName"] = TestSchemas.Ref1.SchemaDef.Name,
["schemaRef1Field"] = "Component1"
}
},
@ -666,11 +681,13 @@ public static class TestContent
new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref1.Id.ToString(),
["schemaName"] = TestSchemas.Ref1.SchemaDef.Name,
["schemaRef1Field"] = "Component1"
},
new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref2.Id.ToString(),
["schemaName"] = TestSchemas.Ref2.SchemaDef.Name,
["schemaRef2Field"] = "Component2"
}
}
@ -683,12 +700,14 @@ public static class TestContent
{
["__typename"] = "MyRefSchema1Component",
["schemaId"] = TestSchemas.Ref1.Id.ToString(),
["schemaName"] = TestSchemas.Ref1.SchemaDef.Name,
["schemaRef1Field"] = "Component1"
},
new Dictionary<string, object>
{
["__typename"] = "MyRefSchema2Component",
["schemaId"] = TestSchemas.Ref2.Id.ToString(),
["schemaName"] = TestSchemas.Ref2.SchemaDef.Name,
["schemaRef2Field"] = "Component2"
}
}
@ -771,11 +790,13 @@ public static class TestContent
["myComponent__Dynamic"] = new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref1.Id.ToString(),
["schemaName"] = TestSchemas.Ref1.SchemaDef.Name,
["schemaRef1Field"] = "Component1"
},
["myComponent"] = new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref1.Id.ToString(),
["schemaName"] = TestSchemas.Ref1.SchemaDef.Name,
["schemaRef1Field"] = "Component1"
},
["myComponents__Dynamic"] = new[]
@ -783,11 +804,13 @@ public static class TestContent
new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref1.Id.ToString(),
["schemaName"] = TestSchemas.Ref1.SchemaDef.Name,
["schemaRef1Field"] = "Component1"
},
new Dictionary<string, object>
{
["schemaId"] = TestSchemas.Ref2.Id.ToString(),
["schemaName"] = TestSchemas.Ref2.SchemaDef.Name,
["schemaRef2Field"] = "Component2"
}
},
@ -797,12 +820,14 @@ public static class TestContent
{
["__typename"] = "MyRefSchema1Component",
["schemaId"] = TestSchemas.Ref1.Id.ToString(),
["schemaName"] = TestSchemas.Ref1.SchemaDef.Name,
["schemaRef1Field"] = "Component1"
},
new Dictionary<string, object>
{
["__typename"] = "MyRefSchema2Component",
["schemaId"] = TestSchemas.Ref2.Id.ToString(),
["schemaName"] = TestSchemas.Ref2.SchemaDef.Name,
["schemaRef2Field"] = "Component2"
}
},

14
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -21,21 +21,21 @@
<ProjectReference Include="..\Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.1" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="GraphQL" Version="7.3.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.3.0" />
<PackageReference Include="FakeItEasy" Version="7.4.0" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="GraphQL" Version="7.5.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.5.0" />
<PackageReference Include="Lorem.Universal.Net" Version="4.0.80" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Reactive.Linq" Version="5.0.0" />
<PackageReference Include="System.Reactive.Linq" Version="6.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">

8
backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj

@ -14,13 +14,13 @@
<ProjectReference Include="..\..\src\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.1" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="FakeItEasy" Version="7.4.0" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />

4
backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs

@ -89,8 +89,8 @@ public class BsonJsonSerializerTests
Bool = true,
Byte = 0x2,
Bytes = new byte[] { 0x10, 0x12, 0x13 },
DateTimeOffset = DateTime.Today,
DateTime = DateTime.UtcNow.Date,
DateTimeOffset = new DateTimeOffset(2022, 12, 11, 10, 9, 8, TimeSpan.Zero),
DateTime = new DateTime(2022, 12, 11),
Float32 = 32.5f,
Float64 = 32.5d,
Guid = Guid.NewGuid(),

11
backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -14,14 +14,17 @@
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.1" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="FakeItEasy" Version="7.4.0" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

8
backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj

@ -12,13 +12,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.3.1" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.22">
<PackageReference Include="FakeItEasy" Version="7.4.0" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">

1
frontend/src/app/features/content/declarations.ts

@ -28,6 +28,7 @@ export * from './shared/due-time-selector.component';
export * from './shared/forms/array-editor.component';
export * from './shared/forms/array-item.component';
export * from './shared/forms/assets-editor.component';
export * from './shared/forms/chat-dialog.component';
export * from './shared/forms/component-section.component';
export * from './shared/forms/component.component';
export * from './shared/forms/field-editor.component';

3
frontend/src/app/features/content/module.ts

@ -9,7 +9,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { VirtualScrollerModule } from 'ngx-virtual-scroller';
import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, LoadSchemasGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldCopyButtonComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceDropdownComponent, ReferenceItemComponent, ReferencesCheckboxesComponent, ReferencesEditorComponent, ReferencesPageComponent, ReferencesTagsComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations';
import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, ChatDialogComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldCopyButtonComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceDropdownComponent, ReferenceItemComponent, ReferencesCheckboxesComponent, ReferencesEditorComponent, ReferencesPageComponent, ReferencesTagsComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations';
const routes: Routes = [
{
@ -97,6 +97,7 @@ const routes: Routes = [
ArrayItemComponent,
AssetsEditorComponent,
CalendarPageComponent,
ChatDialogComponent,
CommentsPageComponent,
ComponentComponent,
ComponentSectionComponent,

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

@ -4,10 +4,6 @@
<div class="languages-container">
<div class="languages-buttons">
<div class="languages-inner">
<button *ngIf="!formModel.field.isDisabled && isTranslatable" type="button" class="btn btn-text-secondary btn-sm no-focus-shadow me-1" title="i18n:contents.autotranslate" (click)="translate()" tabindex="-1">
<i class="icon-translate"></i>
</button>
<sqx-field-languages
[formModel]="formModel"
(languageChange)="languageChange.emit($event)"
@ -18,6 +14,10 @@
</sqx-field-languages>
<sqx-field-copy-button [formModel]="formModel" [languages]="languages"></sqx-field-copy-button>
<button *ngIf="!formModel.field.isDisabled && isTranslatable" type="button" class="btn btn-sm btn-outline-secondary force no-focus-shadow ms-1" title="i18n:contents.autotranslate" (click)="translate()" tabindex="-1">
<i class="icon-translate"></i>
</button>
</div>
</div>
</div>

2
frontend/src/app/features/content/pages/content/editor/content-field.component.scss

@ -31,7 +31,7 @@
}
&-buttons {
@include absolute(-.75rem, 4.25rem, null, 0);
@include absolute(-.75rem, 6.5rem, null, 0);
overflow: hidden;
}

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

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

56
frontend/src/app/features/content/shared/forms/chat-dialog.component.html

@ -0,0 +1,56 @@
<sqx-modal-dialog size="lg" (close)="close.emit()">
<ng-container title>
{{ 'chat.title' | sqxTranslate }}
</ng-container>
<ng-container content>
<sqx-form-alert [marginBottom]="4">
<span [sqxMarkdown]="'chat.description' | sqxTranslate" [inline]="true"></span>
</sqx-form-alert>
<form (ngSubmit)="ask()">
<div class="row row-cols-0 g-2">
<div class="col">
<input class="form-control" placeholder="{{ 'chat.prompt' | sqxTranslate }}"
sqxFocusOnInit
(ngModelChange)="setQuestion($event)"
[ngModel]="snapshot.chatQuestion"
[ngModelOptions]="{ standalone: true }"
[disabled]="snapshot.isRunning" />
</div>
<div class="col-auto">
<button type="submit" class="btn btn-secondary" [disabled]="snapshot.isRunning">
{{ 'chat.ask' | sqxTranslate }}
</button>
</div>
</div>
</form>
<ng-container *ngIf="!snapshot.isRunning; else loading">
<ng-container *ngIf="snapshot.chatAnswers">
<h4 class="mt-4">{{ 'chat.answers' | sqxTranslate }}</h4>
<div *ngIf="snapshot.chatAnswers?.length === 0">
{{ 'chat.noAnswers' | sqxTranslate }}
</div>
<div class="row g-2 answer" *ngFor="let answer of (snapshot.chatAnswers || [])">
<div class="col">
<textarea class="form-control" readonly [ngModel]="answer"></textarea>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary" (click)="complete.emit(answer)">
{{ 'chat.use' | sqxTranslate }}
</button>
</div>
</div>
</ng-container>
</ng-container>
<ng-template #loading>
<div class="mt-4">
<sqx-loader></sqx-loader>
</div>
</ng-template>
</ng-container>
</sqx-modal-dialog>

6
frontend/src/app/features/content/shared/forms/chat-dialog.component.scss

@ -0,0 +1,6 @@
@import 'mixins';
@import 'vars';
textarea {
height: 100px;
}

65
frontend/src/app/features/content/shared/forms/chat-dialog.component.ts

@ -0,0 +1,65 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectorRef, Component, EventEmitter, Output } from '@angular/core';
import { AppsState, StatefulComponent, TranslationsService } from '@app/shared';
interface State {
// True, when running
isRunning: boolean;
// The questions.
chatQuestion: string;
// The answers.
chatAnswers?: ReadonlyArray<string>;
}
@Component({
selector: 'sqx-chat-dialog',
styleUrls: ['./chat-dialog.component.scss'],
templateUrl: './chat-dialog.component.html',
})
export class ChatDialogComponent extends StatefulComponent<State> {
@Output()
public close = new EventEmitter();
@Output()
public complete = new EventEmitter<string>();
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly translator: TranslationsService,
) {
super(changeDetector, {
isRunning: false,
chatQuestion: '',
chatAnswers: undefined,
});
}
public setQuestion(chatQuestion: string) {
this.next({ chatQuestion });
}
public ask() {
this.next({ isRunning: true });
this.translator.ask(this.appsState.appName, { prompt: this.snapshot.chatQuestion })
.subscribe({
next: chatAnswers => {
this.next({ chatAnswers });
},
error: () => {
this.next({ chatAnswers: [] });
},
complete: () => {
this.next({ isRunning: false });
},
});
}
}

13
frontend/src/app/features/content/shared/forms/field-editor.component.html

@ -1,11 +1,15 @@
<div class="field" [class.expanded]="isExpanded" *ngIf="formModel">
<div class="buttons-container" *ngIf="canUnset">
<div class="buttons">
<button type="button" class="btn btn-sm btn-outline-secondary force" title="i18n:contents.fieldFullscreen" (click)="toggleExpanded()" tabindex="-1">
<button type="button" class="btn btn-sm btn-outline-secondary force no-focus-shadow" (click)="chatDialog.show()" tabindex="-1">
AI
</button>
<button type="button" class="btn btn-sm btn-outline-secondary force no-focus-shadow ms-1" title="i18n:contents.fieldFullscreen" (click)="toggleExpanded()" tabindex="-1">
<i class="icon-fullscreen"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary btn-clear force ms-1" [disabled]="isEmpty | async" tabindex="-1"
<button type="button" class="btn btn-sm btn-outline-secondary btn-clear force no-focus-shadow ms-1" [disabled]="isEmpty | async" tabindex="-1"
(sqxConfirmClick)="unset()"
confirmTitle="i18n:contents.unsetValueConfirmTitle"
confirmText="i18n:contents.unsetValueConfirmText"
@ -244,3 +248,8 @@
<span [sqxMarkdown]="field.properties.hints" [optional]="true" [inline]="true"></span>
</sqx-form-hint>
</div>
<ng-container *sqxModal="chatDialog">
<sqx-chat-dialog (close)="chatDialog.hide()" (complete)="setValue($event)"></sqx-chat-dialog>
</ng-container>

10
frontend/src/app/features/content/shared/forms/field-editor.component.ts

@ -8,7 +8,7 @@
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, hasNoValue$, MathHelper, TypedSimpleChanges, Types } from '@app/shared';
import { AbstractContentForm, AppLanguageDto, DialogModel, EditContentForm, FieldDto, hasNoValue$, MathHelper, TypedSimpleChanges, Types } from '@app/shared';
@Component({
selector: 'sqx-field-editor[form][formContext][formLevel][formModel][isComparing][language][languages]',
@ -57,6 +57,8 @@ export class FieldEditorComponent {
public isEmpty?: Observable<boolean>;
public isExpanded = false;
public chatDialog = new DialogModel();
public get field() {
return this.formModel.field;
}
@ -92,4 +94,10 @@ export class FieldEditorComponent {
public unset() {
this.formModel.unset();
}
public setValue(value: any) {
this.formModel.setValue(value);
this.chatDialog.hide();
}
}

8
frontend/src/app/features/settings/pages/templates/template.component.html

@ -20,7 +20,13 @@
</div>
<div class="table-items-row-details-tab">
<div class="help" [innerHTML]="details | async | sqxMarkdown | sqxSafeHtml"></div>
<ng-container *ngIf="details | async; let loadedDetails; else loading">
<div class="help" [innerHTML]="loadedDetails | sqxMarkdown | sqxSafeHtml"></div>
</ng-container>
<ng-template #loading>
<sqx-loader></sqx-loader>
</ng-template>
</div>
</div>
</div>

26
frontend/src/app/shared/services/translations.service.spec.ts

@ -47,4 +47,30 @@ describe('TranslationsService', () => {
expect(translation!).toEqual(new TranslationDto('Translated', 'Hallo'));
}));
it('should make post request to ask question',
inject([TranslationsService, HttpTestingController], (translationsService: TranslationsService, httpMock: HttpTestingController) => {
const dto = { prompt: 'My Question' };
let answers: ReadonlyArray<string>;
translationsService.ask('my-app', dto).subscribe(result => {
answers = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ask');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([
'Answer1',
'Answer2',
]);
expect(answers!).toEqual([
'Answer1',
'Answer2',
]);
}));
});

12
frontend/src/app/shared/services/translations.service.ts

@ -30,6 +30,11 @@ export type TranslateDto = Readonly<{
targetLanguage: string;
}>;
export type AskDto = Readonly<{
// The question to ask.
prompt: string;
}>;
@Injectable()
export class TranslationsService {
constructor(
@ -47,6 +52,13 @@ export class TranslationsService {
}),
pretifyError('i18n:translate.translateFailed'));
}
public ask(appName: string, request: AskDto): Observable<ReadonlyArray<string>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ask`);
return this.http.post<any>(url, request).pipe(
pretifyError('i18n:chatBot.questionFailed'));
}
}
function parseTranslation(body: any): TranslationDto {

12
frontend/src/app/shell/pages/home/home-page.component.ts

@ -38,9 +38,17 @@ export class HomePageComponent {
}
try {
const path = await this.authService.loginPopup(redirectPath) || '/app';
let path = await this.authService.loginPopup(redirectPath);
this.router.navigateByUrl(path, { replaceUrl: true });
if (!path) {
path = '/app';
}
const success = await this.router.navigateByUrl(path, { replaceUrl: true });
if (!success) {
this.router.navigate(['/app'], { replaceUrl: true });
}
} catch {
this.router.navigate(['/'], { replaceUrl: true });
}

Loading…
Cancel
Save