mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
96 changed files with 1224 additions and 201 deletions
@ -0,0 +1,59 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.Json.Objects; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
public static class Updates |
|||
{ |
|||
public static bool IsUnset(object? value) |
|||
{ |
|||
if (value is JsonValue json) |
|||
{ |
|||
return IsUnset(json.Value); |
|||
} |
|||
|
|||
return value is IReadOnlyDictionary<string, JsonValue> obj && IsUnset(obj); |
|||
} |
|||
|
|||
public static bool IsUnset(IReadOnlyDictionary<string, JsonValue>? obj) |
|||
{ |
|||
return |
|||
obj is { Count: 1 } && |
|||
obj.TryGetValue("$unset", out var item) && |
|||
Equals(item.Value, true); |
|||
} |
|||
|
|||
public static bool IsUpdate(object? value, out string expression) |
|||
{ |
|||
expression = null!; |
|||
|
|||
if (value is JsonValue json) |
|||
{ |
|||
return IsUpdate(json.Value, out expression); |
|||
} |
|||
|
|||
return value is IReadOnlyDictionary<string, JsonValue> obj && IsUpdate(obj, out expression); |
|||
} |
|||
|
|||
public static bool IsUpdate(IReadOnlyDictionary<string, JsonValue>? obj, out string expression) |
|||
{ |
|||
expression = null!; |
|||
|
|||
if (obj is { Count: > 0 } && |
|||
obj.TryGetValue("$update", out var item) && |
|||
item.Value is string e && |
|||
!string.IsNullOrWhiteSpace(e)) |
|||
{ |
|||
expression = e; |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.AI; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities; |
|||
|
|||
public sealed class AppChatContext : ChatContext |
|||
{ |
|||
required public Context BaseContext { get; init; } |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using Squidex.AI; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Shared; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps; |
|||
|
|||
public sealed class AppChatTools : IChatToolProvider |
|||
{ |
|||
private readonly IJsonSerializer serializer; |
|||
private readonly IUrlGenerator urlGenerator; |
|||
|
|||
public AppChatTools(IJsonSerializer serializer, IUrlGenerator urlGenerator) |
|||
{ |
|||
this.serializer = serializer; |
|||
this.urlGenerator = urlGenerator; |
|||
} |
|||
|
|||
public async IAsyncEnumerable<IChatTool> GetToolsAsync(ChatContext chatContext, |
|||
[EnumeratorCancellation] CancellationToken ct) |
|||
{ |
|||
if (chatContext is not AppChatContext appContext) |
|||
{ |
|||
yield break; |
|||
} |
|||
|
|||
var context = appContext.BaseContext; |
|||
|
|||
await Task.Yield(); |
|||
|
|||
if (context.Allows(PermissionIds.AppClientsRead)) |
|||
{ |
|||
yield return new DelegateChatTool( |
|||
new ToolSpec("clients", "Clients", "Provides the clients for the Squidex App."), |
|||
(_, ct) => |
|||
{ |
|||
var result = new |
|||
{ |
|||
Clients = context.App.Clients.Select(x => |
|||
new |
|||
{ |
|||
Id = x.Key, |
|||
ClientId = $"{context.App.Name}:{x.Key}", |
|||
ClientSecret = "obfuscated", |
|||
x.Value.Role |
|||
}), |
|||
Url = urlGenerator.ClientsUI(context.App.NamedId()) |
|||
}; |
|||
|
|||
var json = serializer.Serialize(result, true); |
|||
|
|||
return Task.FromResult(json); |
|||
}); |
|||
} |
|||
|
|||
if (context.Allows(PermissionIds.AppLanguagesRead)) |
|||
{ |
|||
yield return new DelegateChatTool( |
|||
new ToolSpec("languages", "Languages", "Provides the languages for the Squidex App."), |
|||
(_, ct) => |
|||
{ |
|||
var result = new |
|||
{ |
|||
Languages = context.App.Languages.Values.Select(x => |
|||
new |
|||
{ |
|||
Iso2Code = x.Key, |
|||
IsMaster = context.App.Languages.Master.Equals(x.Key), |
|||
x.Value.IsOptional |
|||
}), |
|||
Url = urlGenerator.LanguagesUI(context.App.NamedId()) |
|||
}; |
|||
|
|||
var json = serializer.Serialize(result, true); |
|||
|
|||
return Task.FromResult(json); |
|||
}); |
|||
} |
|||
|
|||
if (context.Allows(PermissionIds.AppRolesRead)) |
|||
{ |
|||
yield return new DelegateChatTool( |
|||
new ToolSpec("roles", "Roles", "Provides the roles for the Squidex App."), |
|||
(_, ct) => |
|||
{ |
|||
var result = new |
|||
{ |
|||
Roles = context.App.Roles.Custom.Select(x => |
|||
new |
|||
{ |
|||
x.Name |
|||
}), |
|||
Url = urlGenerator.RolesUI(context.App.NamedId()) |
|||
}; |
|||
|
|||
var json = serializer.Serialize(result, true); |
|||
|
|||
return Task.FromResult(json); |
|||
}); |
|||
} |
|||
|
|||
if (context.Allows(PermissionIds.AppPlansRead)) |
|||
{ |
|||
yield return new DelegateChatTool( |
|||
new ToolSpec("plan", "Plan", "Provides the plan for the Squidex App."), |
|||
(_, ct) => |
|||
{ |
|||
var result = new |
|||
{ |
|||
Plan = new |
|||
{ |
|||
Name = context.App.Plan?.PlanId, |
|||
}, |
|||
Url = urlGenerator.PlansUI(context.App.NamedId()) |
|||
}; |
|||
|
|||
var json = serializer.Serialize(result, true); |
|||
|
|||
return Task.FromResult(json); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using Squidex.AI; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Shared; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Schemas; |
|||
|
|||
public sealed class SchemasChatTool : IChatToolProvider |
|||
{ |
|||
private readonly IAppProvider appProvider; |
|||
private readonly IJsonSerializer serializer; |
|||
private readonly IUrlGenerator urlGenerator; |
|||
|
|||
public SchemasChatTool(IAppProvider appProvider, IJsonSerializer serializer, IUrlGenerator urlGenerator) |
|||
{ |
|||
this.appProvider = appProvider; |
|||
this.serializer = serializer; |
|||
this.urlGenerator = urlGenerator; |
|||
} |
|||
|
|||
public async IAsyncEnumerable<IChatTool> GetToolsAsync(ChatContext chatContext, |
|||
[EnumeratorCancellation] CancellationToken ct) |
|||
{ |
|||
if (chatContext is not AppChatContext appContext) |
|||
{ |
|||
yield break; |
|||
} |
|||
|
|||
var context = appContext.BaseContext; |
|||
|
|||
await Task.Yield(); |
|||
|
|||
if (context.Allows(PermissionIds.AppSchemasRead)) |
|||
{ |
|||
yield return new DelegateChatTool( |
|||
new ToolSpec("schemas", "Schemas", "Provides the schemas for the Squidex App."), |
|||
async (_, ct) => |
|||
{ |
|||
var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); |
|||
|
|||
var result = new |
|||
{ |
|||
Schemas = schemas.Select(x => |
|||
new |
|||
{ |
|||
x.Name, |
|||
x.IsPublished, |
|||
x.Type, |
|||
FieldCount = x.Fields.Count, |
|||
Url = urlGenerator.SchemaUI(context.App.NamedId(), x.NamedId()) |
|||
}), |
|||
Url = urlGenerator.SchemasUI(context.App.NamedId()) |
|||
}; |
|||
|
|||
var json = serializer.Serialize(result, true); |
|||
|
|||
return json; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,146 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.TestHelpers; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Shared; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps; |
|||
|
|||
public class AppChatToolsTests : GivenContext |
|||
{ |
|||
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); |
|||
private readonly AppChatTools sut; |
|||
|
|||
public AppChatToolsTests() |
|||
{ |
|||
sut = new AppChatTools(TestUtils.DefaultSerializer, urlGenerator); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_clients_if_user_has_permission() |
|||
{ |
|||
App = App with |
|||
{ |
|||
Clients = App.Clients.Add("default", "secret") |
|||
}; |
|||
|
|||
var chatContext = new AppChatContext |
|||
{ |
|||
BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppClientsRead, App.Name).Id) |
|||
}; |
|||
|
|||
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); |
|||
|
|||
Assert.NotNull(tool); |
|||
Assert.Equal("clients", tool.Spec.Name); |
|||
Assert.Equal("Clients", tool.Spec.DisplayName); |
|||
|
|||
var result = await tool.ExecuteAsync(null!, default); |
|||
|
|||
Assert.Contains($"{App.Name}:default", result); |
|||
|
|||
A.CallTo(() => urlGenerator.ClientsUI(AppId)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_languages_if_user_has_permission() |
|||
{ |
|||
App = App with |
|||
{ |
|||
Languages = App.Languages.Set(Language.DE) |
|||
}; |
|||
|
|||
var chatContext = new AppChatContext |
|||
{ |
|||
BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppLanguages, App.Name).Id) |
|||
}; |
|||
|
|||
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); |
|||
|
|||
Assert.NotNull(tool); |
|||
Assert.Equal("languages", tool.Spec.Name); |
|||
Assert.Equal("Languages", tool.Spec.DisplayName); |
|||
|
|||
var result = await tool.ExecuteAsync(null!, default); |
|||
|
|||
Assert.Contains($"\"de\"", result); |
|||
|
|||
A.CallTo(() => urlGenerator.LanguagesUI(AppId)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_roles_if_user_has_permission() |
|||
{ |
|||
App = App with |
|||
{ |
|||
Roles = App.Roles.Add("viewers") |
|||
}; |
|||
|
|||
var chatContext = new AppChatContext |
|||
{ |
|||
BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppRoles, App.Name).Id) |
|||
}; |
|||
|
|||
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); |
|||
|
|||
Assert.NotNull(tool); |
|||
Assert.Equal("roles", tool.Spec.Name); |
|||
Assert.Equal("Roles", tool.Spec.DisplayName); |
|||
|
|||
var result = await tool.ExecuteAsync(null!, default); |
|||
|
|||
Assert.Contains($"viewers", result); |
|||
|
|||
A.CallTo(() => urlGenerator.RolesUI(AppId)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_plan_if_user_has_permission() |
|||
{ |
|||
App = App with |
|||
{ |
|||
Plan = new AssignedPlan(User, "Business") |
|||
}; |
|||
|
|||
var chatContext = new AppChatContext |
|||
{ |
|||
BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppPlans, App.Name).Id) |
|||
}; |
|||
|
|||
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); |
|||
|
|||
Assert.NotNull(tool); |
|||
Assert.Equal("plan", tool.Spec.Name); |
|||
Assert.Equal("Plan", tool.Spec.DisplayName); |
|||
|
|||
var result = await tool.ExecuteAsync(null!, default); |
|||
|
|||
Assert.Contains($"Business", result); |
|||
|
|||
A.CallTo(() => urlGenerator.PlansUI(AppId)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_return_tools_if_user_no_permission() |
|||
{ |
|||
var chatContext = new AppChatContext |
|||
{ |
|||
BaseContext = FrontendContext |
|||
}; |
|||
|
|||
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); |
|||
|
|||
Assert.Null(tool); |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using MongoDB.Bson.Serialization; |
|||
using MongoDB.Driver; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents; |
|||
using ExtensionSut = Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations.Extensions; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; |
|||
|
|||
public class ExtensionsTests |
|||
{ |
|||
public ExtensionsTests() |
|||
{ |
|||
MongoContentEntity.RegisterClassMap(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_build_projection_without_fields() |
|||
{ |
|||
var projection = ExtensionSut.BuildProjection2<MongoContentEntity>(null); |
|||
|
|||
AssertProjection(projection, "{ 'dd' : 0 }"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_build_projection_with_data_prefix() |
|||
{ |
|||
var projection = ExtensionSut.BuildProjection2<MongoContentEntity>(["data.myField"]); |
|||
|
|||
AssertProjection(projection, "{ '_ai' : 1, '_id' : 1, '_si' : 1, 'ai' : 1, 'cb' : 1, 'ct' : 1, 'dl' : 1, 'do.myField' : 1, 'id' : 1, 'is' : 1, 'mb' : 1, 'mt' : 1, 'ns' : 1, 'rf' : 1, 'sa' : 1, 'si' : 1, 'sj' : 1, 'ss' : 1, 'ts' : 1, 'vs' : 1 }"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_build_projection_without_data_prefix() |
|||
{ |
|||
var projection = ExtensionSut.BuildProjection2<MongoContentEntity>(["myField"]); |
|||
|
|||
AssertProjection(projection, "{ '_ai' : 1, '_id' : 1, '_si' : 1, 'ai' : 1, 'cb' : 1, 'ct' : 1, 'dl' : 1, 'do.myField' : 1, 'id' : 1, 'is' : 1, 'mb' : 1, 'mt' : 1, 'ns' : 1, 'rf' : 1, 'sa' : 1, 'si' : 1, 'sj' : 1, 'ss' : 1, 'ts' : 1, 'vs' : 1 }"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_build_projection_without_included_field() |
|||
{ |
|||
var projection = ExtensionSut.BuildProjection2<MongoContentEntity>(["myField.special", "myField"]); |
|||
|
|||
AssertProjection(projection, "{ '_ai' : 1, '_id' : 1, '_si' : 1, 'ai' : 1, 'cb' : 1, 'ct' : 1, 'dl' : 1, 'do.myField' : 1, 'id' : 1, 'is' : 1, 'mb' : 1, 'mt' : 1, 'ns' : 1, 'rf' : 1, 'sa' : 1, 'si' : 1, 'sj' : 1, 'ss' : 1, 'ts' : 1, 'vs' : 1 }"); |
|||
} |
|||
|
|||
private static void AssertProjection(ProjectionDefinition<MongoContentEntity, MongoContentEntity> projection, string expected) |
|||
{ |
|||
var rendered = |
|||
projection.Render( |
|||
BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(), |
|||
BsonSerializer.SerializerRegistry) |
|||
.Document.ToString(); |
|||
|
|||
Assert.Equal(Cleanup(expected), rendered); |
|||
} |
|||
|
|||
private static string Cleanup(string filter) |
|||
{ |
|||
return filter.Replace('\'', '"'); |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.TestHelpers; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Shared; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Schemas; |
|||
|
|||
public class SchemasChatToolTests : GivenContext |
|||
{ |
|||
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); |
|||
private readonly SchemasChatTool sut; |
|||
|
|||
public SchemasChatToolTests() |
|||
{ |
|||
sut = new SchemasChatTool(AppProvider, TestUtils.DefaultSerializer, urlGenerator); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_schemas_if_user_has_permission() |
|||
{ |
|||
var chatContext = new AppChatContext |
|||
{ |
|||
BaseContext = CreateContext(PermissionIds.ForApp(PermissionIds.AppSchemasRead, App.Name).Id) |
|||
}; |
|||
|
|||
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); |
|||
|
|||
Assert.NotNull(tool); |
|||
Assert.Equal("schemas", tool.Spec.Name); |
|||
Assert.Equal("Schemas", tool.Spec.DisplayName); |
|||
|
|||
var result = await tool.ExecuteAsync(null!, default); |
|||
|
|||
Assert.Contains(Schema.Name, result); |
|||
|
|||
A.CallTo(() => urlGenerator.SchemasUI(AppId)) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => urlGenerator.SchemaUI(AppId, SchemaId)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_return_tools_if_user_no_permission() |
|||
{ |
|||
var chatContext = new AppChatContext |
|||
{ |
|||
BaseContext = FrontendContext |
|||
}; |
|||
|
|||
var tool = await sut.GetToolsAsync(chatContext, default).FirstOrDefaultAsync(); |
|||
|
|||
Assert.Null(tool); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
@if ((appsState.selectedApp | async) && hasChatBot) { |
|||
<ul class="nav navbar-nav align-items-center"> |
|||
<li class="nav-item nav-icon" (click)="chatDialog.show()"> |
|||
<span class="nav-link">AI</span> |
|||
</li> |
|||
</ul> |
|||
} |
|||
|
|||
<sqx-chat-dialog (contentSelect)="chatDialog.hide()" *sqxModal="chatDialog"></sqx-chat-dialog> |
|||
@ -0,0 +1,7 @@ |
|||
@import 'mixins'; |
|||
@import 'vars'; |
|||
|
|||
.nav-link { |
|||
font-weight: 450; |
|||
font-size: 1.3rem; |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { AsyncPipe } from '@angular/common'; |
|||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; |
|||
import { AppsState, ChatDialogComponent, DialogModel, ModalDirective, UIOptions } from '@app/shared'; |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
selector: 'sqx-chat-menu', |
|||
styleUrls: ['./chat-menu.component.scss'], |
|||
templateUrl: './chat-menu.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
imports: [ |
|||
AsyncPipe, |
|||
ChatDialogComponent, |
|||
ModalDirective |
|||
], |
|||
}) |
|||
export class ChatMenuComponent { |
|||
public readonly chatDialog = new DialogModel(); |
|||
|
|||
public readonly hasChatBot = inject(UIOptions).value.canUseChatBot; |
|||
|
|||
constructor( |
|||
public readonly appsState: AppsState |
|||
) { |
|||
} |
|||
} |
|||
Loading…
Reference in new issue