Browse Source

Move to Phenx (#1224)

* Move to Phenx

* Fix chat.

* Fix tour.

* Fix geography.
pull/1225/head
Sebastian Stehle 12 months ago
committed by GitHub
parent
commit
939ca6eecd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/EFAssetFolderRepository_SnapshotStore.cs
  2. 3
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/EFAssetRepository_SnapshotStore.cs
  3. 2
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentEntity.cs
  4. 18
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_SnapshotStore.cs
  5. 10
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs
  6. 19
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexerState.cs
  7. 3
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/History/EFHistoryEventRepository.cs
  8. 29
      backend/src/Squidex.Data.EntityFramework/Infrastructure/BulkInserter.cs
  9. 24
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs
  10. 3
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Log/EFRequestLogRepository.cs
  11. 2
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/ConnectionStringParser.cs
  12. 3
      backend/src/Squidex.Data.EntityFramework/Infrastructure/States/EFSnapshotStore.cs
  13. 3
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/.editorconfig
  14. 3
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.cs
  15. 7
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs
  16. 3
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/.editorconfig
  17. 2
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs
  18. 3
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/.editorconfig
  19. 3
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.cs
  20. 7
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs
  21. 3
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/.editorconfig
  22. 2
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs
  23. 12
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/CustomMigrations/AddFlows_Before.cs
  24. 3
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/.editorconfig
  25. 3
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.cs
  26. 7
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs
  27. 3
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/.editorconfig
  28. 2
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs
  29. 5
      backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs
  30. 35
      backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj
  31. 12
      backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj
  32. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  33. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  34. 14
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  35. 5
      backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
  36. 24
      backend/src/Squidex/Squidex.csproj
  37. 9
      backend/tests/Squidex.Data.Tests.CodeGenerator/FixtureTemplate.handlebar
  38. 12
      backend/tests/Squidex.Data.Tests.CodeGenerator/TestTemplate.handlebar
  39. 47
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/BulkHelper.cs
  40. 5
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs
  41. 5
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs
  42. 5
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs
  43. 21
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs
  44. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Apps/MongoAppRepositoryTests.cs
  45. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetFolderRepositorySnapshotTests.cs
  46. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositorySnapshotTests.cs
  47. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositoryTests.cs
  48. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryDedicatedTests.cs
  49. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotDedicatedTests.cs
  50. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotTests.cs
  51. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryTests.cs
  52. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexTests.cs
  53. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexerStateTests.cs
  54. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/History/MongoHistoryEventRepositoryTests.cs
  55. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Rules/MongoRuleRepositoryTests.cs
  56. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemaRepositoryTests.cs
  57. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemasHashTests.cs
  58. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Domain/Teams/MongoTeamRepositoryTests.cs
  59. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Caching/MongoDistributedCacheTests.cs
  60. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Log/MongoRequestLogRepositoryTests.cs
  61. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Migrations/MongoMigrationStatusTests.cs
  62. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/States/MongoSnapshotStoreTests.cs
  63. 2
      backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/UsageTracking/MongoUsageRepositoryTests.cs
  64. 3
      backend/tests/Squidex.Data.Tests/MongoDb/TestHelpers/MongoFixture.cs
  65. 5
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs
  66. 17
      frontend/src/app/features/apps/pages/onboarding-dialog.component.html
  67. 2
      frontend/src/app/features/settings/pages/contributors/contributors-page.component.html
  68. 13
      frontend/src/app/framework/angular/modals/tour-step.directive.ts
  69. 2
      frontend/src/app/framework/angular/modals/tour-template.component.html
  70. 96
      frontend/src/app/framework/angular/modals/tour.service.ts
  71. 43
      frontend/src/app/shared/components/chat-dialog.component.ts
  72. 8
      frontend/src/app/shared/components/chat-item.component.html
  73. 2
      frontend/src/app/shared/components/chat-item.component.ts
  74. 4
      frontend/src/app/shared/components/tour-guide.component.html
  75. 1
      frontend/src/app/shared/components/tour-guide.component.scss
  76. 4
      frontend/src/app/shared/components/tour-guide.component.ts
  77. 2
      frontend/src/app/shared/services/rules.service.ts
  78. 12
      frontend/src/app/shared/services/translations.service.ts
  79. 3
      frontend/src/app/shared/state/tour.state.ts
  80. 7
      frontend/src/app/shared/state/tour.tasks.ts
  81. 2
      frontend/src/app/shell/pages/internal/chat-menu.component.html
  82. 3
      frontend/src/app/shell/pages/internal/chat-menu.component.ts
  83. 4
      frontend/src/app/theme/_bootstrap.scss
  84. 4
      frontend/src/app/theme/_common.scss

3
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/EFAssetFolderRepository_SnapshotStore.cs

@ -7,7 +7,6 @@
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Squidex.Domain.Apps.Core.Apps;
@ -105,7 +104,7 @@ public sealed partial class EFAssetFolderRepository<TContext> : ISnapshotStore<A
}
await using var dbContext = await CreateDbContextAsync(ct);
await dbContext.BulkInsertAsync(entities, cancellationToken: ct);
await dbContext.BulkInsertAsync(entities, ct);
}
}

3
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Assets/EFAssetRepository_SnapshotStore.cs

@ -7,7 +7,6 @@
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Squidex.Domain.Apps.Core.Apps;
@ -105,7 +104,7 @@ public sealed partial class EFAssetRepository<TContext> : ISnapshotStore<Asset>,
}
await using var dbContext = await CreateDbContextAsync(ct);
await dbContext.BulkInsertAsync(entities, cancellationToken: ct);
await dbContext.BulkInsertAsync(entities, ct);
}
}

2
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentEntity.cs

@ -188,7 +188,7 @@ public record EFContentEntity : Content, IVersionedEntity<DomainId>
if (data.CanHaveReference())
{
var components = await appProvider.GetComponentsAsync(schema, ct: ct);
var components = await appProvider.GetComponentsAsync(schema, ct);
data.AddReferencedIds(schema, referencedIds, components);
}

18
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository_SnapshotStore.cs

@ -7,7 +7,6 @@
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
@ -334,10 +333,10 @@ public sealed partial class EFContentRepository<TContext, TContentContext> : ISn
}
}
await dbContext.BulkInsertAsync(writesToCompleteContents, cancellationToken: ct);
await dbContext.BulkInsertAsync(writesToCompleteReferences, cancellationToken: ct);
await dbContext.BulkInsertAsync(writesToPublishedContents, cancellationToken: ct);
await dbContext.BulkInsertAsync(writesToPublishedReferences, cancellationToken: ct);
await dbContext.BulkInsertAsync(writesToCompleteContents, ct);
await dbContext.BulkInsertAsync(writesToCompleteReferences, ct);
await dbContext.BulkInsertAsync(writesToPublishedContents, ct);
await dbContext.BulkInsertAsync(writesToPublishedReferences, ct);
await dbContext.SaveChangesAsync(ct);
if (dedicatedTables)
@ -347,10 +346,13 @@ public sealed partial class EFContentRepository<TContext, TContentContext> : ISn
var contentDbContext = await dynamicTables.CreateDbContextAsync(bySchema.Key.AppId, bySchema.Key.SchemaId, ct);
// Just fetch the published context, so that we can reuse the context.
var publishedContents = writesToPublishedContents.Where(x => x.AppId.Id == bySchema.Key.AppId && x.SchemaId.Id == bySchema.Key.SchemaId);
var publishedContents =
writesToPublishedContents
.Where(x => x.AppId.Id == bySchema.Key.AppId && x.SchemaId.Id == bySchema.Key.SchemaId)
.ToList();
await contentDbContext.BulkInsertAsync(bySchema, cancellationToken: ct);
await contentDbContext.BulkInsertAsync(publishedContents, cancellationToken: ct);
await contentDbContext.BulkInsertAsync(bySchema.ToList(), ct);
await contentDbContext.BulkInsertAsync(publishedContents, ct);
}
}

10
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndex.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using EFCore.BulkExtensions;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using NetTopologySuite.Geometries;
@ -87,7 +86,10 @@ public sealed class EFTextIndex<TContext>(IDbContextFactory<TContext> dbContextF
await using var dbContext = await CreateDbContextAsync(ct);
var point = new Point(query.Longitude, query.Latitude) { SRID = 4326 };
var point = new Point(query.Longitude, query.Latitude)
{
SRID = 4326,
};
// The distance must be converted to decrees (in contrast to MongoDB, which uses radian).
var degrees = query.Radius / 111320;
@ -291,8 +293,8 @@ public sealed class EFTextIndex<TContext>(IDbContextFactory<TContext> dbContextF
}
}
await dbContext.BulkInsertOrUpdateAsync(insertsText, cancellationToken: ct);
await dbContext.BulkInsertOrUpdateAsync(insertsGeo, cancellationToken: ct);
await dbContext.BulkUpsertAsync(insertsText, ct);
await dbContext.BulkUpsertAsync(insertsGeo, ct);
}
private Task<TContext> CreateDbContextAsync(CancellationToken ct)

19
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/Text/EFTextIndexerState.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Schemas;
@ -90,14 +89,14 @@ public sealed class EFTextIndexerState<TContext>(IDbContextFactory<TContext> dbC
public async Task SetAsync(List<TextContentState> updates,
CancellationToken ct = default)
{
var toDelete = new List<TextContentState>();
var toDelete = new List<UniqueContentId>();
var toUpsert = new List<TextContentState>();
foreach (var update in updates)
{
if (update.State == TextState.Deleted)
{
toDelete.Add(update);
toDelete.Add(update.UniqueContentId);
}
else
{
@ -111,8 +110,18 @@ public sealed class EFTextIndexerState<TContext>(IDbContextFactory<TContext> dbC
}
await using var dbContext = await CreateDbContextAsync(ct);
await dbContext.BulkDeleteAsync(toDelete, cancellationToken: ct);
await dbContext.BulkInsertOrUpdateAsync(toUpsert, cancellationToken: ct);
if (toUpsert.Count > 0)
{
await dbContext.BulkUpsertAsync(toUpsert, ct);
}
if (toDelete.Count > 0)
{
await dbContext.Set<TextContentState>()
.Where(x => toDelete.Contains(x.UniqueContentId))
.ExecuteDeleteAsync(ct);
}
}
private Task<TContext> CreateDbContextAsync(CancellationToken ct)

3
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/History/EFHistoryEventRepository.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.History.Repositories;
@ -65,7 +64,7 @@ public sealed class EFHistoryEventRepository<TContext>(IDbContextFactory<TContex
}
await using var dbContext = await CreateDbContextAsync(ct);
await dbContext.BulkInsertOrUpdateAsync(entities, cancellationToken: ct);
await dbContext.BulkUpsertAsync(entities, ct);
}
private Task<TContext> CreateDbContextAsync(CancellationToken ct)

29
backend/src/Squidex.Data.EntityFramework/Infrastructure/BulkInserter.cs

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
using PhenX.EntityFrameworkCore.BulkInsert.Options;
using Squidex.Events.EntityFramework;
using Squidex.Flows.EntityFramework;
namespace Squidex.Infrastructure;
public sealed class BulkInserter : IDbFlowsBulkInserter, IDbEventStoreBulkInserter
{
public Task BulkInsertAsync<T>(DbContext dbContext, IEnumerable<T> entities,
CancellationToken ct = default) where T : class
{
return dbContext.ExecuteBulkInsertAsync(entities, cancellationToken: ct);
}
public Task BulkUpsertAsync<T>(DbContext dbContext, IEnumerable<T> entities,
CancellationToken ct = default) where T : class
{
return dbContext.ExecuteBulkInsertAsync(entities, o => { }, new OnConflictOptions<T> { Update = e => e }, ct);
}
}

24
backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs

@ -10,6 +10,8 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.DependencyInjection;
using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
using PhenX.EntityFrameworkCore.BulkInsert.Options;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
@ -60,6 +62,28 @@ public static class Extensions
return source.Where(predicate);
}
public static Task BulkUpsertAsync<T>(this DbContext dbContext, List<T> source,
CancellationToken ct) where T : class
{
if (source.Count == 0)
{
return Task.CompletedTask;
}
return dbContext.ExecuteBulkInsertAsync(source, o => { }, new OnConflictOptions<T> { Update = e => e }, ct);
}
public static Task BulkInsertAsync<T>(this DbContext dbContext, List<T> source,
CancellationToken ct) where T : class
{
if (source.Count == 0)
{
return Task.CompletedTask;
}
return dbContext.ExecuteBulkInsertAsync(source, cancellationToken: ct);
}
public static async Task<IResultList<T>> QueryAsync<T>(this IQueryable<T> queryable, Q q,
CancellationToken ct) where T : class
{

3
backend/src/Squidex.Data.EntityFramework/Infrastructure/Log/EFRequestLogRepository.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using NodaTime;
@ -72,7 +71,7 @@ public sealed class EFRequestLogRepository<TContext>(IDbContextFactory<TContext>
await using var dbContext = await CreateDbContextAsync(ct);
await dbContext.BulkInsertAsync(entities, cancellationToken: ct);
await dbContext.BulkInsertAsync(entities, ct);
}
public async IAsyncEnumerable<Request> QueryAllAsync(string key, Instant fromTime, Instant toTime,

2
backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/ConnectionStringParser.cs

@ -28,7 +28,7 @@ public class ConnectionStringParser
{
var builder = new DbConnectionStringBuilder
{
ConnectionString = source
ConnectionString = source,
};
if (builder.TryGetValue("Server", out var server))

3
backend/src/Squidex.Data.EntityFramework/Infrastructure/States/EFSnapshotStore.cs

@ -7,7 +7,6 @@
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
@ -106,7 +105,7 @@ public class EFSnapshotStore<TContext, T, TState>(IDbContextFactory<TContext> db
}
await using var dbContext = await CreateDbContextAsync(ct);
await dbContext.BulkInsertOrUpdateAsync(entities, cancellationToken: ct);
await dbContext.BulkUpsertAsync(entities, ct);
}
}

3
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/.editorconfig

@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none
# MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none

3
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250513192106_AddCronJobs.cs

@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

7
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.MySql;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
@ -15,4 +16,10 @@ public sealed class MySqlAppDbContext(DbContextOptions options, IJsonSerializer
: AppDbContext(options, jsonSerializer)
{
public override SqlDialect Dialect => MySqlDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseBulkInsertMySql();
base.OnConfiguring(optionsBuilder);
}
}

3
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/Migrations/.editorconfig

@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none
# MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none

2
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.MySql;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
@ -27,6 +28,7 @@ public sealed class MySqlContentDbContext(string prefix, string connectionString
ServerVersion.AutoDetect(connectionString);
optionsBuilder.SetDefaultWarnings();
optionsBuilder.UseBulkInsertMySql();
optionsBuilder.UseMySql(connectionString, version, options =>
{
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);

3
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/.editorconfig

@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none
# MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none

3
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250513192113_AddCronJobs.cs

@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

7
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
@ -15,4 +16,10 @@ public class PostgresAppDbContext(DbContextOptions options, IJsonSerializer json
: AppDbContext(options, jsonSerializer)
{
public override SqlDialect Dialect => PostgresDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseBulkInsertPostgreSql();
base.OnConfiguring(optionsBuilder);
}
}

3
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/Migrations/.editorconfig

@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none
# MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none

2
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
@ -21,6 +22,7 @@ public sealed class PostgresContentDbContext(string prefix, string connectionStr
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseBulkInsertPostgreSql();
optionsBuilder.SetDefaultWarnings();
optionsBuilder.UseNpgsql(connectionString, options =>
{

12
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/CustomMigrations/AddFlows_Before.cs

@ -17,15 +17,15 @@ internal class AddFlows_Before : Migration
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_MessagingData",
table: "MessagingData");
"PK_MessagingData",
"MessagingData");
migrationBuilder.DropPrimaryKey(
name: "PK_Chats",
table: "Chats");
"PK_Chats",
"Chats");
migrationBuilder.DropPrimaryKey(
name: "PK_AssetKeyValueStore_TusMetadata",
table: "AssetKeyValueStore_TusMetadata");
"PK_AssetKeyValueStore_TusMetadata",
"AssetKeyValueStore_TusMetadata");
}
}

3
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/.editorconfig

@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none
# MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none

3
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250513192120_AddCronJobs.cs

@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

7
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
@ -15,4 +16,10 @@ public sealed class SqlServerAppDbContext(DbContextOptions options, IJsonSeriali
: AppDbContext(options, jsonSerializer)
{
public override SqlDialect Dialect => SqlServerDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseBulkInsertSqlServer();
base.OnConfiguring(optionsBuilder);
}
}

3
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/Migrations/.editorconfig

@ -5,5 +5,8 @@ dotnet_diagnostic.MA0007.severity = none
# MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none

2
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
@ -22,6 +23,7 @@ public sealed class SqlServerContentDbContext(string prefix, string connectionSt
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.SetDefaultWarnings();
optionsBuilder.UseBulkInsertSqlServer();
optionsBuilder.UseSqlServer(connectionString, options =>
{
options.MigrationsHistoryTable($"{prefix}MigrationHistory");

5
backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs

@ -36,6 +36,8 @@ using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Domain.Apps.Entities.Teams;
using Squidex.Domain.Apps.Entities.Teams.Repositories;
using Squidex.Domain.Users;
using Squidex.Events.EntityFramework;
using Squidex.Flows.EntityFramework;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log;
@ -226,6 +228,9 @@ public static class ServiceExtensions
services.AddSingletonAs<EFUserFactory>()
.As<IUserFactory>();
services.AddSingletonAs<BulkInserter>()
.As<IDbEventStoreBulkInserter>().As<IDbFlowsBulkInserter>();
services.AddFlowsCore()
.AddEntityFrameworkStore<TContext, FlowEventContext>();

35
backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj

@ -25,29 +25,28 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="8.0.12" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="8.0.16" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.16" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="8.0.11" />
<PackageReference Include="Npgsql.OpenTelemetry" Version="8.0.7" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.Json.Microsoft" Version="8.0.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.NetTopologySuite" Version="8.0.2" />
<PackageReference Include="PhenX.EntityFrameworkCore.BulkInsert.MySql" Version="0.2.3" />
<PackageReference Include="PhenX.EntityFrameworkCore.BulkInsert.PostgreSql" Version="0.2.3" />
<PackageReference Include="PhenX.EntityFrameworkCore.BulkInsert.SqlServer" Version="0.2.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.Json.Microsoft" Version="8.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.NetTopologySuite" Version="8.0.3" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.EntityFramework" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.EntityFramework" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.18.0" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.Core" Version="8.1.6" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.MySql" Version="8.1.6" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.PostgreSQL" Version="8.1.6" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.SqlServer" Version="8.1.6" />
<PackageReference Include="Squidex.Events.EntityFramework" Version="7.18.0" />
<PackageReference Include="Squidex.Flows.EntityFramework" Version="7.18.0" />
<PackageReference Include="Squidex.Hosting" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.EntityFramework" Version="7.18.0" />
<PackageReference Include="Squidex.AI.EntityFramework" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.EntityFramework" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.22.0" />
<PackageReference Include="Squidex.Events.EntityFramework" Version="7.22.0" />
<PackageReference Include="Squidex.Flows.EntityFramework" Version="7.22.0" />
<PackageReference Include="Squidex.Hosting" Version="7.22.0" />
<PackageReference Include="Squidex.Messaging.EntityFramework" Version="7.22.0" />
<PackageReference Include="Squidex.OpenIdDict.EntityFramework" Version="5.8.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

12
backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj

@ -25,12 +25,12 @@
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.30.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.Mongo" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="7.18.0" />
<PackageReference Include="Squidex.Events.Mongo" Version="7.18.0" />
<PackageReference Include="Squidex.Flows.Mongo" Version="7.18.0" />
<PackageReference Include="Squidex.Hosting" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.Mongo" Version="7.18.0" />
<PackageReference Include="Squidex.AI.Mongo" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="7.22.0" />
<PackageReference Include="Squidex.Events.Mongo" Version="7.22.0" />
<PackageReference Include="Squidex.Flows.Mongo" Version="7.22.0" />
<PackageReference Include="Squidex.Hosting" Version="7.22.0" />
<PackageReference Include="Squidex.Messaging.Mongo" Version="7.22.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="5.8.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

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

@ -20,7 +20,7 @@
<PackageReference Include="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Flows" Version="7.18.0" />
<PackageReference Include="Squidex.Flows" Version="7.22.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

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

@ -29,8 +29,8 @@
<PackageReference Include="NJsonSchema" Version="11.0.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.18.0" />
<PackageReference Include="Squidex.AI" Version="7.22.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.22.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

14
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -24,13 +24,13 @@
<PackageReference Include="NodaTime" Version="3.2.0" />
<PackageReference Include="OpenTelemetry.Api" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="7.18.0" />
<PackageReference Include="Squidex.Caching" Version="7.18.0" />
<PackageReference Include="Squidex.Events" Version="7.18.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="7.18.0" />
<PackageReference Include="Squidex.Log" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging" Version="7.18.0" />
<PackageReference Include="Squidex.Text" Version="7.18.0" />
<PackageReference Include="Squidex.Assets" Version="7.22.0" />
<PackageReference Include="Squidex.Caching" Version="7.22.0" />
<PackageReference Include="Squidex.Events" Version="7.22.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="7.22.0" />
<PackageReference Include="Squidex.Log" Version="7.22.0" />
<PackageReference Include="Squidex.Messaging" Version="7.22.0" />
<PackageReference Include="Squidex.Text" Version="7.22.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

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

@ -77,6 +77,7 @@ public sealed class TranslationsController(ICommandBus commandBus, IAssetStore a
{
var chatRequest = new ChatRequest
{
LoadHistory = true,
Configuration = request.Configuration,
ConversationId = request.ConversationId,
Prompt = request.Prompt,
@ -105,6 +106,10 @@ public sealed class TranslationsController(ICommandBus commandBus, IAssetStore a
case ToolEndEvent toolEnd:
json = new { type = "ToolEnd", tool = toolEnd.Tool.Spec.DisplayName };
break;
case ChatHistoryLoaded historyLoaded:
var message = historyLoaded.Message;
json = new { type = "History", content = message.Content, source = message.Type.ToString() };
break;
}
if (json != null)

24
backend/src/Squidex/Squidex.csproj

@ -59,17 +59,17 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.4.1" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.S3" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.Azure" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.S3" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.22.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="21.5.0" />
<PackageReference Include="Squidex.Events.GetEventStore" Version="7.18.0" />
<PackageReference Include="Squidex.Hosting" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.All" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.18.0" />
<PackageReference Include="Squidex.Events.GetEventStore" Version="7.22.0" />
<PackageReference Include="Squidex.Hosting" Version="7.22.0" />
<PackageReference Include="Squidex.Messaging.All" Version="7.22.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.22.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="YDotNet" Version="0.4.3" />
<PackageReference Include="YDotNet.Native" Version="0.4.3" />
@ -83,11 +83,11 @@
</ItemGroup>
<ItemGroup Condition="'$(IncludeMagick)' == 'true'">
<PackageReference Include="Squidex.Assets.ImageMagick" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="7.22.0" />
</ItemGroup>
<ItemGroup Condition="'$(IncludeKafka)' == 'true'">
<PackageReference Include="Squidex.Messaging.Kafka" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.Kafka" Version="7.22.0" />
</ItemGroup>
<PropertyGroup>

9
backend/tests/Squidex.Data.Tests.CodeGenerator/FixtureTemplate.handlebar

@ -2,27 +2,30 @@
// Auto-generated code
namespace Squidex.EntityFramework.TestHelpers;
[CollectionDefinition("Postgres{{Name}}")]
[CollectionDefinition(Name)]
public sealed class Postgres{{Name}}FixtureCollection : ICollectionFixture<Postgres{{Name}}Fixture>
{
public const string Name = "Postgres{{Name}}";
}
public sealed class Postgres{{Name}}Fixture() : PostgresFixture("squidex-postgres-{{Label}}")
{
}
[CollectionDefinition("MySql{{Name}}")]
[CollectionDefinition(Name)]
public sealed class MySql{{Name}}FixtureCollection : ICollectionFixture<MySql{{Name}}Fixture>
{
public const string Name = "MySql{{Name}}";
}
public sealed class MySql{{Name}}Fixture() : MySqlFixture("squidex-mysql-{{Label}}")
{
}
[CollectionDefinition("SqlServer{{Name}}")]
[CollectionDefinition(Name)]
public sealed class SqlServer{{Name}}FixtureCollection : ICollectionFixture<SqlServer{{Name}}Fixture>
{
public const string Name = "SqlServer{{Name}}";
}
public sealed class SqlServer{{Name}}Fixture() : SqlServerFixture("squidex-mssql-{{Label}}")

12
backend/tests/Squidex.Data.Tests.CodeGenerator/TestTemplate.handlebar

@ -11,37 +11,37 @@ namespace {{classNamespace}};
{{#if HasContentContext}}
[Trait("Category", "TestContainer")]
[Collection("Postgres{{CollectionSuffix}}")]
[Collection(Postgres{{CollectionSuffix}}FixtureCollection.Name)]
public class Postgres{{className}}(Postgres{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextPostgres, PostgresContentDbContext>(fixture)
{
}
[Trait("Category", "TestContainer")]
[Collection("MySql{{CollectionSuffix}}")]
[Collection(MySql{{CollectionSuffix}}FixtureCollection.Name)]
public class MySql{{className}}(MySql{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextMySql, MySqlContentDbContext>(fixture)
{
}
[Trait("Category", "TestContainer")]
[Collection("SqlServer{{CollectionSuffix}}")]
[Collection(SqlServer{{CollectionSuffix}}FixtureCollection.Name)]
public class SqlServer{{className}}(SqlServer{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextSqlServer, SqlServerContentDbContext>(fixture)
{
}
{{else}}
[Trait("Category", "TestContainer")]
[Collection("Postgres{{CollectionSuffix}}")]
[Collection(Postgres{{CollectionSuffix}}FixtureCollection.Name)]
public class Postgres{{className}}(Postgres{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextPostgres>(fixture)
{
}
[Trait("Category", "TestContainer")]
[Collection("MySql{{CollectionSuffix}}")]
[Collection(MySql{{CollectionSuffix}}FixtureCollection.Name)]
public class MySql{{className}}(MySql{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextMySql>(fixture)
{
}
[Trait("Category", "TestContainer")]
[Collection("SqlServer{{CollectionSuffix}}")]
[Collection(SqlServer{{CollectionSuffix}}FixtureCollection.Name)]
public class SqlServer{{className}}(SqlServer{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextSqlServer>(fixture)
{
}

47
backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/BulkHelper.cs

@ -1,47 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using EFCore.BulkExtensions.SqlAdapters;
using EFCore.BulkExtensions.SqlAdapters.MySql;
using EFCore.BulkExtensions.SqlAdapters.PostgreSql;
using EFCore.BulkExtensions.SqlAdapters.SqlServer;
using Squidex.Providers.MySql.Content;
using Squidex.Providers.Postgres.Content;
using Squidex.Providers.SqlServer.Content;
namespace Squidex.EntityFramework.TestHelpers;
public static class BulkHelper
{
private static readonly IDbServer MySql = new MySqlDbServer();
private static readonly IDbServer PostgreSql = new PostgreSqlDbServer();
private static readonly IDbServer SqlServer = new SqlServerDbServer();
public static void Configure()
{
SqlAdaptersMapping.Provider = context =>
{
switch (context)
{
case MySqlContentDbContext:
return MySql;
case PostgresContentDbContext:
return PostgreSql;
case SqlServerContentDbContext:
return SqlServer;
case TestDbContextMySql:
return MySql;
case TestDbContextPostgres:
return PostgreSql;
case TestDbContextSqlServer:
return SqlServer;
}
throw new ArgumentException("Not supported.", nameof(context));
};
}
}

5
backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs

@ -34,11 +34,6 @@ public class MySqlFixture(string? reuseId = null) : IAsyncLifetime, ISqlContentF
public IDbContextNamedFactory<MySqlContentDbContext> DbContextNamedFactory
=> services.GetRequiredService<IDbContextNamedFactory<MySqlContentDbContext>>();
static MySqlFixture()
{
BulkHelper.Configure();
}
public async Task InitializeAsync()
{
await mysql.StartAsync();

5
backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs

@ -34,11 +34,6 @@ public class PostgresFixture(string? reuseId) : IAsyncLifetime, ISqlContentFixtu
public IDbContextNamedFactory<PostgresContentDbContext> DbContextNamedFactory
=> services.GetRequiredService<IDbContextNamedFactory<PostgresContentDbContext>>();
static PostgresFixture()
{
BulkHelper.Configure();
}
public async Task InitializeAsync()
{
await postgreSql.StartAsync();

5
backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs

@ -35,11 +35,6 @@ public class SqlServerFixture(string? reuseId = null) : IAsyncLifetime, ISqlCont
public IDbContextNamedFactory<SqlServerContentDbContext> DbContextNamedFactory
=> services.GetRequiredService<IDbContextNamedFactory<SqlServerContentDbContext>>();
static SqlServerFixture()
{
BulkHelper.Configure();
}
public async Task InitializeAsync()
{
await sqlServer.StartAsync();

21
backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs

@ -6,6 +6,9 @@
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.MySql;
using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
using PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries;
@ -24,18 +27,36 @@ public class TestDbContextMySql(DbContextOptions options, IJsonSerializer jsonSe
: TestDbContext(options, jsonSerializer)
{
public override SqlDialect Dialect => MySqlDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseBulkInsertMySql();
base.OnConfiguring(optionsBuilder);
}
}
public class TestDbContextPostgres(DbContextOptions options, IJsonSerializer jsonSerializer)
: TestDbContext(options, jsonSerializer)
{
public override SqlDialect Dialect => PostgresDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseBulkInsertPostgreSql();
base.OnConfiguring(optionsBuilder);
}
}
public class TestDbContextSqlServer(DbContextOptions options, IJsonSerializer jsonSerializer)
: TestDbContext(options, jsonSerializer)
{
public override SqlDialect Dialect => SqlServerDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseBulkInsertSqlServer();
base.OnConfiguring(optionsBuilder);
}
}
public abstract class TestDbContext(DbContextOptions options, IJsonSerializer jsonSerializer)

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Apps/MongoAppRepositoryTests.cs

@ -13,7 +13,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Apps;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoAppRepositoryTests(MongoFixture fixture) : AppRepositoryTests
{
protected override async Task<IAppRepository> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetFolderRepositorySnapshotTests.cs

@ -14,7 +14,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Assets;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoAssetFolderRepositorySnapshotTests(MongoFixture fixture) : AssetFolderSnapshotStoreTests
{
protected override async Task<ISnapshotStore<AssetFolder>> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositorySnapshotTests.cs

@ -15,7 +15,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Assets;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoAssetRepositorySnapshotTests(MongoFixture fixture) : AssetSnapshotStoreTests
{
protected override async Task<ISnapshotStore<Asset>> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Assets/MongoAssetRepositoryTests.cs

@ -14,7 +14,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Assets;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoAssetRepositoryTests(MongoFixture fixture) : AssetRepositoryTests
{
protected override async Task<IAssetRepository> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryDedicatedTests.cs

@ -16,7 +16,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Contents;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoContentRepositoryDedicatedTests(MongoFixture fixture) : ContentRepositoryTests
{
protected override async Task<IContentRepository> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotDedicatedTests.cs

@ -17,7 +17,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Contents;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoContentRepositorySnapshotDedicatedTests(MongoFixture fixture) : ContentSnapshotStoreTests
{
protected override bool CheckConsistencyOnWrite => false;

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositorySnapshotTests.cs

@ -17,7 +17,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Contents;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoContentRepositorySnapshotTests(MongoFixture fixture) : ContentSnapshotStoreTests
{
protected override bool CheckConsistencyOnWrite => false;

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/MongoContentRepositoryTests.cs

@ -16,7 +16,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Contents;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoContentRepositoryTests(MongoFixture fixture) : ContentRepositoryTests
{
protected override async Task<IContentRepository> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexTests.cs

@ -11,7 +11,7 @@ using Squidex.MongoDb.TestHelpers;
namespace Squidex.MongoDb.Domain.Contents.Text;
[Trait("Category", "Dependencies")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoTextIndexTests(MongoFixture fixture) : TextIndexerTests
{
public override bool SupportsQuerySyntax => false;

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Contents/Text/MongoTextIndexerStateTests.cs

@ -14,7 +14,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Contents.Text;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoTextIndexerStateTests(MongoFixture fixture) : TextIndexerStateTests
{
protected override async Task<ITextIndexerState> CreateSutAsync(IContentRepository contentRepository)

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/History/MongoHistoryEventRepositoryTests.cs

@ -13,7 +13,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.History;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoHistoryEventRepositoryTests(MongoFixture fixture) : HistoryEventRepositoryTests
{
protected override async Task<IHistoryEventRepository> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Rules/MongoRuleRepositoryTests.cs

@ -13,7 +13,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Rules;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoRuleRepositoryTests(MongoFixture fixture) : RuleRepositoryTests
{
protected override async Task<IRuleRepository> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemaRepositoryTests.cs

@ -13,7 +13,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Schemas;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoSchemaRepositoryTests(MongoFixture fixture) : SchemaRepositoryTests
{
protected override async Task<ISchemaRepository> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Schemas/MongoSchemasHashTests.cs

@ -19,7 +19,7 @@ using Squidex.MongoDb.TestHelpers;
namespace Squidex.MongoDb.Domain.Schemas;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoSchemasHashTests(MongoFixture fixture) : GivenContext, IAsyncLifetime
{
private readonly MongoSchemasHash sut = new MongoSchemasHash(fixture.Database);

2
backend/tests/Squidex.Data.Tests/MongoDb/Domain/Teams/MongoTeamRepositoryTests.cs

@ -13,7 +13,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Domain.Teams;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoTeamRepositoryTests(MongoFixture fixture) : TeamRepositoryTests
{
protected override async Task<ITeamRepository> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Caching/MongoDistributedCacheTests.cs

@ -13,7 +13,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Infrastructure.Caching;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoDistributedCacheTests(MongoFixture fixture) : DistributedCacheTests
{
protected override async Task<IDistributedCache> CreateSutAsync(TimeProvider timeProvider)

2
backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Log/MongoRequestLogRepositoryTests.cs

@ -13,7 +13,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Infrastructure.Log;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoRequestLogRepositoryTests(MongoFixture fixture) : RequestLogRepositoryTests
{
protected override async Task<IRequestLogRepository> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/Migrations/MongoMigrationStatusTests.cs

@ -12,7 +12,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Infrastructure.Migrations;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoMigrationStatusTests(MongoFixture fixture) : MigrationStatusTests
{
protected override async Task<IMigrationStatus> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/States/MongoSnapshotStoreTests.cs

@ -12,7 +12,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Infrastructure.States;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoSnapshotStoreTests(MongoFixture fixture) : SnapshotStoreTests
{
protected override async Task<ISnapshotStore<SnapshotValue>> CreateSutAsync()

2
backend/tests/Squidex.Data.Tests/MongoDb/Infrastructure/UsageTracking/MongoUsageRepositoryTests.cs

@ -12,7 +12,7 @@ using Squidex.Shared;
namespace Squidex.MongoDb.Infrastructure.UsageTracking;
[Trait("Category", "TestContainer")]
[Collection("Mongo")]
[Collection(MongoFixtureCollection.Name)]
public class MongoUsageRepositoryTests(MongoFixture fixture) : UsageRepositoryTests
{
protected override async Task<IUsageRepository> CreateSutAsync()

3
backend/tests/Squidex.Data.Tests/MongoDb/TestHelpers/MongoFixture.cs

@ -12,9 +12,10 @@ using Testcontainers.MongoDb;
namespace Squidex.MongoDb.TestHelpers;
[CollectionDefinition("Mongo")]
[CollectionDefinition(Name)]
public sealed class MongoFixtureCollection : ICollectionFixture<MongoFixture>
{
public const string Name = "Mongo";
}
public class MongoFixture : IAsyncLifetime

5
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests.cs

@ -439,11 +439,10 @@ public abstract class TextIndexerTests : GivenContext
{
var query = new GeoQuery(SchemaId.Id, field, latitude, longitude, 1000, 1000)
{
SchemaId = default,
SchemaId = SchemaId.Id,
};
var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected));
AssertIds(actual, expected);
}
@ -455,7 +454,6 @@ public abstract class TextIndexerTests : GivenContext
};
var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected));
AssertIds(actual, expected);
}
@ -467,7 +465,6 @@ public abstract class TextIndexerTests : GivenContext
};
var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected));
AssertIds(actual, expected);
}

17
frontend/src/app/features/apps/pages/onboarding-dialog.component.html

@ -12,7 +12,7 @@
{{ "tour.welcome" | sqxTranslate }} <span class="header-focus">{{ "tour.welcomeProduct" | sqxTranslate }}</span>
</h1>
<div [sqxMarkdown]="'tour.stepIntroText' | sqxTranslate"></div>
<div inline="false" [sqxMarkdown]="'tour.stepIntroText' | sqxTranslate"></div>
<div class="mt-4">
<button class="btn btn-success" (click)="next()">{{ "tour.stepIntroNext" | sqxTranslate }}</button>
@ -34,15 +34,10 @@
<label for="role">{{ "tour.stepDataCompanyRole" | sqxTranslate }}</label>
<select class="form-select" id="companyRole" formControlName="companyRole">
<option [ngValue]="'RoleEmployee'">{{ "tour.roleEmployee" | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessOwner'">{{ "tour.roleBusinessOwner" | sqxTranslate }}</option>
<option [ngValue]="'RoleProductManager'">{{ "tour.roleProductManager" | sqxTranslate }}</option>
<option [ngValue]="'RoleContentCreator'">{{ "tour.roleContentCreator" | sqxTranslate }}</option>
<option [ngValue]="'RoleSoftwareDeveloper'">{{ "tour.roleSoftwareDeveloper" | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessAnalyst'">{{ "tour.roleBusinessAnalyst" | sqxTranslate }}</option>
</select>
</div>
@ -51,13 +46,9 @@
<label for="companySize">{{ "tour.stepDataCompanySize" | sqxTranslate }}</label>
<select class="form-select" id="companySize" formControlName="companySize">
<option [ngValue]="'SizeSingle'">{{ "tour.sizeSingle" | sqxTranslate }}</option>
<option [ngValue]="'SizeSmall'">{{ "tour.sizeSmall" | sqxTranslate }}</option>
<option [ngValue]="'SizeMedium'">{{ "tour.sizeMedium" | sqxTranslate }}</option>
<option [ngValue]="'SizeLarge'">{{ "tour.sizeLarge" | sqxTranslate }}</option>
<option [ngValue]="'SizeVeryLarge'">{{ "tour.sizeVeryLarge" | sqxTranslate }}</option>
</select>
</div>
@ -66,17 +57,11 @@
<label for="project">{{ "tour.stepDataProject" | sqxTranslate }}</label>
<select class="form-select" id="project" formControlName="project">
<option [ngValue]="'ProjectNewsMagazine'">{{ "tour.projectNewsMagazine" | sqxTranslate }}</option>
<option [ngValue]="'ProjectPersonalBlog'">{{ "tour.projectPersonalBlog" | sqxTranslate }}</option>
<option [ngValue]="'ProjectSmallBusiness'">{{ "tour.projectSmallBusiness" | sqxTranslate }}</option>
<option [ngValue]="'ProjectCommerce'">{{ "tour.projectCommerce" | sqxTranslate }}</option>
<option [ngValue]="'ProjectMobileApp'">{{ "tour.projectMobileApp" | sqxTranslate }}</option>
<option [ngValue]="'ProjectBackend'">{{ "tour.projectBackend" | sqxTranslate }}</option>
<option [ngValue]="'ProjectLearning'">{{ "tour.projectLearning" | sqxTranslate }}</option>
</select>
</div>

2
frontend/src/app/features/settings/pages/contributors/contributors-page.component.html

@ -62,7 +62,7 @@
replaceUrl="true"
routerLink="history"
routerLinkActive="active"
sqxTourStep="help"
sqxTourStep="history"
title="i18n:common.history"
titlePosition="left">
<i class="icon-time"></i>

13
frontend/src/app/framework/angular/modals/tour-step.directive.ts

@ -16,6 +16,7 @@ import { StepDefinition, TourService } from './tour.service';
export class TourStepDirective implements OnInit, OnDestroy, TourAnchorDirective {
private isNextOnClick = false;
private isActive = false;
private currentAnchorId?: string | null;
private wasClicked = false;
@Input({ alias: 'sqxTourStep', required: true })
@ -27,28 +28,30 @@ export class TourStepDirective implements OnInit, OnDestroy, TourAnchorDirective
}
public ngOnInit(): void {
if (!this.anchorId) {
this.currentAnchorId = this.anchorId;
if (!this.currentAnchorId) {
return;
}
this.tourService.register(this.anchorId, this);
this.tourService.register(this.currentAnchorId, this);
}
public ngOnDestroy(): void {
if (!this.anchorId) {
if (!this.currentAnchorId) {
return;
}
if (this.isActive && (!this.isNextOnClick || !this.wasClicked)) {
setTimeout(() => {
if (this.tourService.currentStep.anchorId === this.anchorId) {
if (this.tourService.currentStep.anchorId === this.currentAnchorId) {
this.tourService.render(null, null);
this.tourService.pause();
}
}, 200);
}
this.tourService.unregister(this.anchorId);
this.tourService.unregister(this.currentAnchorId);
}
@HostListener('click')

2
frontend/src/app/framework/angular/modals/tour-template.component.html

@ -19,7 +19,7 @@
<div class="col">
<h5 inline="true" [sqxMarkdown]="currentStep.title | sqxTranslate"></h5>
<div [sqxMarkdown]="currentStep.content | sqxTranslate"></div>
<div [sqxMarkdown]="currentStep.content | sqxTranslate" inline="false"></div>
</div>
<div class="col-auto"><button class="btn btn-sm btn-close" (click)="tourService.end()" type="button"></button></div>

96
frontend/src/app/framework/angular/modals/tour.service.ts

@ -6,7 +6,7 @@
*/
import { Injectable } from '@angular/core';
import { TourService as BaseTourService, IStepOption, TourState } from 'ngx-ui-tour-core';
import { TourService as BaseTourService, IStepOption, TourAnchorDirective, TourState } from 'ngx-ui-tour-core';
import { filter, Observable, Subscription, take } from 'rxjs';
import { FloatingPlacement } from '@app/framework/internal';
import { TourTemplateComponent } from './tour-template.component';
@ -21,8 +21,11 @@ export interface StepDefinition extends IStepOption {
// Additional callback.
hideThis?: () => void;
// Goes to the end automatically.
endOnCondition?: ((service: TourService, anchor: TourAnchorDirective) => Observable<any>) | null;
// Goes to the next element automatically.
nextOnCondition?: ((service: TourService) => Observable<any>) | null;
nextOnCondition?: ((service: TourService, anchor: TourAnchorDirective) => Observable<any>) | null;
}
export function waitForAnchor(anchorId: string) {
@ -34,11 +37,69 @@ export function waitForAnchor(anchorId: string) {
};
}
export function waitForAnchorClick() {
return (_: TourService, anchor: TourAnchorDirective) => {
return new Observable<boolean>(subscriber => {
const element = anchor.element.nativeElement as HTMLElement;
const listener = () => {
subscriber.next(true);
subscriber.complete();
element.removeEventListener('click', listener);
};
element.addEventListener('click', listener);
return () => {
element.removeEventListener('click', listener);
};
});
};
}
export function waitForElement(selector: string) {
return () => {
return new Observable<boolean>(subscriber => {
const observer = new MutationObserver((mutationsList) => {
let shouldUpdate = false;
for (const mutation of mutationsList) {
if (mutation.type === 'childList' || mutation.type === 'attributes') {
shouldUpdate = true;
break;
}
}
if (shouldUpdate) {
const element = document.querySelector(selector);
if (element) {
subscriber.next(true);
subscriber.complete();
observer.disconnect();
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class'],
});
return () => {
observer.disconnect();
};
});
};
}
@Injectable({
providedIn: 'root',
})
export class TourService extends BaseTourService<StepDefinition> {
private condition?: Subscription;
private onNext?: Subscription;
private onEnd?: Subscription;
public component?: TourTemplateComponent | null = null;
@ -55,10 +116,24 @@ export class TourService extends BaseTourService<StepDefinition> {
document.body.style.overflow = 'auto';
});
this.stepShow$
.subscribe(({ step }) => {
const directive = this.anchors[step.anchorId!];
this.onNext = step.nextOnCondition?.(this, directive)?.subscribe(() => {
this.goto(this.steps.indexOf(step) + 1);
});
this.onEnd = step.endOnCondition?.(this, directive)?.subscribe(() => {
this.end();
});
});
this.stepHide$
.subscribe(() => {
if (this.getStatus() !== TourState.PAUSED) {
this.condition?.unsubscribe();
this.onNext?.unsubscribe();
this.onEnd?.unsubscribe();
}
});
}
@ -66,17 +141,4 @@ export class TourService extends BaseTourService<StepDefinition> {
public render(step: StepDefinition | null, target: any | null) {
this.component?.render(step, target);
}
public run(steps: StepDefinition[]) {
this.initialize(steps);
this.start();
}
protected showStep(step: StepDefinition): Promise<void> {
this.condition = step.nextOnCondition?.(this)?.subscribe(() => {
this.goto(this.steps.indexOf(step) + 1);
});
return super.showStep(step);
}
}

43
frontend/src/app/shared/components/chat-dialog.component.ts

@ -8,7 +8,7 @@
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Observable } from 'rxjs';
import { Observable, ReplaySubject, Subject } from 'rxjs';
import { HTTP, MathHelper, ModalDialogComponent, TooltipDirective, TranslatePipe } from '@app/framework';
import { AppsState, AuthService, ChatEventDto, StatefulComponent, TranslationsService } from '@app/shared/internal';
import { ChatItemComponent } from './chat-item.component';
@ -21,7 +21,7 @@ interface State {
isRunning: boolean;
// The answers.
chatItems: ReadonlyArray<{ content: string | Observable<ChatEventDto>; type: 'User' | 'Bot' | 'System' }>;
chatItems: ReadonlyArray<{ content: string | Observable<ChatEventDto>; type: 'User' | 'Assistant' | 'System' }>;
}
@Component({
@ -38,14 +38,15 @@ interface State {
],
})
export class ChatDialogComponent extends StatefulComponent<State> {
private readonly conversationId = MathHelper.guid();
@Output()
public contentSelect = new EventEmitter<string | HTTP.UploadFile | undefined | null>();
@Input()
public configuration?: string;
@Input()
public conversationId = MathHelper.guid();
@Input()
public folderId?: string;
@ -71,14 +72,32 @@ export class ChatDialogComponent extends StatefulComponent<State> {
public ngOnInit() {
const { configuration, conversationId } = this;
const stream = this.translator.ask(this.appsState.appName, { conversationId, configuration });
this.next(s => ({
...s,
chatQuestion: '',
chatItems: [...s.chatItems, { content: stream, type: 'Bot' }],
isRunning: true,
}));
let observable: Subject<ChatEventDto>;
this.translator.ask(this.appsState.appName, { conversationId, configuration })
.subscribe(message => {
if (message.type === 'History') {
const { content, source } = message;
this.next(s => ({
...s,
chatItems: [...s.chatItems, { content, type: source }],
}));
} else {
if (!observable) {
observable = new ReplaySubject<ChatEventDto>();
this.next(s => ({
...s,
chatQuestion: '',
chatItems: [...s.chatItems, { content: observable, type: 'Assistant' }],
isRunning: true,
}));
}
observable.next(message);
}
});
}
public setQuestion(chatQuestion: string) {
@ -105,7 +124,7 @@ export class ChatDialogComponent extends StatefulComponent<State> {
chatItems: [
...s.chatItems,
{ content: prompt, type: 'User' },
{ content: stream, type: 'Bot' },
{ content: stream, type: 'Assistant' },
],
isRunning: true,
}));

8
frontend/src/app/shared/components/chat-item.component.html

@ -20,7 +20,7 @@
</div>
}
@if (type === "Bot") {
@if (type === "Assistant") {
<div class="row mt-3">
<div class="col-auto">
<div class="squid squid-sm d-flex align-items-center justify-content-center"><img src="./images/squid.svg" /></div>
@ -45,7 +45,7 @@
<span> {{ "chat.failed" | sqxTranslate }} </span>
}
@if (!snapshot.isRunning && !isFirst && type === "Bot") {
@if (!snapshot.isRunning && !isFirst && type === "Assistant") {
<button class="btn btn-secondary btn-sm btn-text" (click)="selectContent()" [disabled]="snapshot.isCopying" type="button">
{{ "chat.use" | sqxTranslate }}
@if (snapshot.isCopying) {
@ -55,12 +55,10 @@
}
</div>
@if (snapshot.isRunning) {
@if (snapshot.isRunning && !snapshot.content) {
<svg class="loader" height="10" viewBox="0 0 40 16">
<circle class="dot" cx="8" cy="8" r="4" />
<circle class="dot" cx="20" cy="8" r="4" />
<circle class="dot" cx="32" cy="8" r="4" />
</svg>
}

2
frontend/src/app/shared/components/chat-item.component.ts

@ -49,7 +49,7 @@ export class ChatItemComponent extends StatefulComponent<State> {
public focusElement!: ElementRef<HTMLElement>;
@Input({ required: true })
public type: 'Bot' | 'User' | 'System' = 'Bot';
public type: 'Assistant' | 'User' | 'System' = 'Assistant';
@Input({ required: true })
public folderId?: string;

4
frontend/src/app/shared/components/tour-guide.component.html

@ -45,18 +45,14 @@
<a href="https://squidex.io/help" target="_blank">
<div class="row g-0 align-items-center">
<div class="col-auto"><i class="icon-help"></i></div>
<div class="col ps-4">{{ "tour.documentation" | sqxTranslate }}</div>
<div class="col-auto"><i class="icon-angle-right"></i></div>
</div>
</a>
<a href="https://support.squidex.io" target="_blank">
<div class="row g-0 align-items-center">
<div class="col-auto"><i class="icon-user-o"></i></div>
<div class="col ps-4">{{ "tour.support" | sqxTranslate }}</div>
<div class="col-auto"><i class="icon-angle-right"></i></div>
</div>
</a>

1
frontend/src/app/shared/components/tour-guide.component.scss

@ -77,6 +77,7 @@ $caret-size: 14px;
.summary {
align-items: center;
color: $color-white;
cursor: pointer;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;

4
frontend/src/app/shared/components/tour-guide.component.ts

@ -63,10 +63,6 @@ export class TourGuideComponent extends StatefulComponent<State> implements OnIn
}
public start(task: TaskSnapshot) {
if (!task.isActive) {
return;
}
this.tourState.runTask(task);
}
}

2
frontend/src/app/shared/services/rules.service.ts

@ -180,7 +180,7 @@ export class RulesService {
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url, {}).pipe(
return this.http.request(link.method, url, { body: {} }).pipe(
pretifyError('i18n:rules.triggerFailed'));
}

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

@ -31,6 +31,16 @@ export interface ChatChunkDto {
content: string;
}
export interface ChatHistoryLoaded {
type: 'History';
// The content of the chunk.
content: string;
// Defines where the message was created from.
source: 'User' | 'Assistant';
}
export interface ChatToolStartDto {
type: 'ToolStart';
@ -45,7 +55,7 @@ export interface ChatToolEndDto {
tool: string;
}
export type ChatEventDto = ChatChunkDto | ChatToolStartDto | ChatToolEndDto;
export type ChatEventDto = ChatChunkDto | ChatToolStartDto | ChatToolEndDto | ChatHistoryLoaded;
@Injectable({

3
frontend/src/app/shared/state/tour.state.ts

@ -96,7 +96,8 @@ export class TourState extends State<Snapshot> {
return;
}
this.tourService.run(task.steps);
this.tourService.initialize(task.steps);
this.tourService.start();
}
public disableAllHints() {

7
frontend/src/app/shared/state/tour.tasks.ts

@ -7,7 +7,7 @@
import { inject, InjectionToken } from '@angular/core';
import { filter, Observable, take } from 'rxjs';
import { MessageBus, StepDefinition, waitForAnchor } from '@app/framework';
import { MessageBus, StepDefinition, waitForAnchor, waitForAnchorClick, waitForElement } from '@app/framework';
import { ClientTourStated, QueryExecuted } from '../utils/messages';
import { AppsState } from './apps.state';
import { AssetsState } from './assets.state';
@ -128,11 +128,13 @@ export function buildTasks() {
}, {
anchorId: 'help',
content: 'i18n:tour.createSchema.helpContent',
endOnCondition: waitForElement('.panel2.minimized .right'),
scrollContainer: '.panel-container',
position: 'left-start',
}, {
anchorId: 'history',
content: 'i18n:tour.createSchema.historyContent',
endOnCondition: waitForElement('.panel2.minimized .right'),
scrollContainer: '.panel-container',
position: 'left-start',
}],
@ -183,6 +185,8 @@ export function buildTasks() {
}, {
anchorId: 'status',
content: 'i18n:tour.createContent.statusContent',
endOnCondition: waitForAnchorClick(),
nextOnAnchorClick: true,
isAsync: true,
position: 'left-start',
}],
@ -221,6 +225,7 @@ export function buildTasks() {
anchorId: 'filter',
content: 'i18n:tour.createAsset.filterContent',
position: 'left-start',
endOnCondition: waitForElement('.panel2.minimized .right'),
}],
onComplete: (() => {
const assetsState = inject(AssetsState);

2
frontend/src/app/shell/pages/internal/chat-menu.component.html

@ -3,4 +3,4 @@
<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 (contentSelect)="chatDialog.hide()" [conversationId]="authService.user!.id" *sqxModal="chatDialog" />

3
frontend/src/app/shell/pages/internal/chat-menu.component.ts

@ -7,7 +7,7 @@
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { AppsState, ChatDialogComponent, DialogModel, ModalDirective, UIOptions } from '@app/shared';
import { AppsState, AuthService, ChatDialogComponent, DialogModel, ModalDirective, UIOptions } from '@app/shared';
@Component({
standalone: true,
@ -28,6 +28,7 @@ export class ChatMenuComponent {
constructor(
public readonly appsState: AppsState,
public readonly authService: AuthService,
) {
}
}

4
frontend/src/app/theme/_bootstrap.scss

@ -70,6 +70,10 @@
a {
color: inherit;
}
ul {
margin: 0;
}
}
.alert-hint {

4
frontend/src/app/theme/_common.scss

@ -17,6 +17,10 @@ body {
}
}
html {
height: 100vh;
}
hr {
border-color: $color-border;
}

Loading…
Cancel
Save