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.Linq.Expressions;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query;
using Squidex.Domain.Apps.Core.Apps; 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 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.Linq.Expressions;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query;
using Squidex.Domain.Apps.Core.Apps; 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 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()) if (data.CanHaveReference())
{ {
var components = await appProvider.GetComponentsAsync(schema, ct: ct); var components = await appProvider.GetComponentsAsync(schema, ct);
data.AddReferencedIds(schema, referencedIds, components); 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.Linq.Expressions;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; 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(writesToCompleteContents, ct);
await dbContext.BulkInsertAsync(writesToCompleteReferences, cancellationToken: ct); await dbContext.BulkInsertAsync(writesToCompleteReferences, ct);
await dbContext.BulkInsertAsync(writesToPublishedContents, cancellationToken: ct); await dbContext.BulkInsertAsync(writesToPublishedContents, ct);
await dbContext.BulkInsertAsync(writesToPublishedReferences, cancellationToken: ct); await dbContext.BulkInsertAsync(writesToPublishedReferences, ct);
await dbContext.SaveChangesAsync(ct); await dbContext.SaveChangesAsync(ct);
if (dedicatedTables) 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); var contentDbContext = await dynamicTables.CreateDbContextAsync(bySchema.Key.AppId, bySchema.Key.SchemaId, ct);
// Just fetch the published context, so that we can reuse the context. // 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(bySchema.ToList(), ct);
await contentDbContext.BulkInsertAsync(publishedContents, cancellationToken: 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using EFCore.BulkExtensions;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NetTopologySuite.Geometries; using NetTopologySuite.Geometries;
@ -87,7 +86,10 @@ public sealed class EFTextIndex<TContext>(IDbContextFactory<TContext> dbContextF
await using var dbContext = await CreateDbContextAsync(ct); 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). // The distance must be converted to decrees (in contrast to MongoDB, which uses radian).
var degrees = query.Radius / 111320; 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.BulkUpsertAsync(insertsText, ct);
await dbContext.BulkInsertOrUpdateAsync(insertsGeo, cancellationToken: ct); await dbContext.BulkUpsertAsync(insertsGeo, ct);
} }
private Task<TContext> CreateDbContextAsync(CancellationToken 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Schemas; 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, public async Task SetAsync(List<TextContentState> updates,
CancellationToken ct = default) CancellationToken ct = default)
{ {
var toDelete = new List<TextContentState>(); var toDelete = new List<UniqueContentId>();
var toUpsert = new List<TextContentState>(); var toUpsert = new List<TextContentState>();
foreach (var update in updates) foreach (var update in updates)
{ {
if (update.State == TextState.Deleted) if (update.State == TextState.Deleted)
{ {
toDelete.Add(update); toDelete.Add(update.UniqueContentId);
} }
else else
{ {
@ -111,8 +110,18 @@ public sealed class EFTextIndexerState<TContext>(IDbContextFactory<TContext> dbC
} }
await using var dbContext = await CreateDbContextAsync(ct); 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) 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.History.Repositories; 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 using var dbContext = await CreateDbContextAsync(ct);
await dbContext.BulkInsertOrUpdateAsync(entities, cancellationToken: ct); await dbContext.BulkUpsertAsync(entities, ct);
} }
private Task<TContext> CreateDbContextAsync(CancellationToken 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.Diagnostics;
using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
using PhenX.EntityFrameworkCore.BulkInsert.Options;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -60,6 +62,28 @@ public static class Extensions
return source.Where(predicate); 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, public static async Task<IResultList<T>> QueryAsync<T>(this IQueryable<T> queryable, Q q,
CancellationToken ct) where T : class CancellationToken ct) where T : class
{ {

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

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NodaTime; using NodaTime;
@ -72,7 +71,7 @@ public sealed class EFRequestLogRepository<TContext>(IDbContextFactory<TContext>
await using var dbContext = await CreateDbContextAsync(ct); 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, 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 var builder = new DbConnectionStringBuilder
{ {
ConnectionString = source ConnectionString = source,
}; };
if (builder.TryGetValue("Server", out var server)) 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.Linq.Expressions;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query; 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 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 # MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header # SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none 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 #nullable disable

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.MySql;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -15,4 +16,10 @@ public sealed class MySqlAppDbContext(DbContextOptions options, IJsonSerializer
: AppDbContext(options, jsonSerializer) : AppDbContext(options, jsonSerializer)
{ {
public override SqlDialect Dialect => MySqlDialect.Instance; 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 # MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header # SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none dotnet_diagnostic.SA1633.severity = none

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.MySql;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -27,6 +28,7 @@ public sealed class MySqlContentDbContext(string prefix, string connectionString
ServerVersion.AutoDetect(connectionString); ServerVersion.AutoDetect(connectionString);
optionsBuilder.SetDefaultWarnings(); optionsBuilder.SetDefaultWarnings();
optionsBuilder.UseBulkInsertMySql();
optionsBuilder.UseMySql(connectionString, version, options => optionsBuilder.UseMySql(connectionString, version, options =>
{ {
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); 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 # MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header # SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none 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 #nullable disable

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -15,4 +16,10 @@ public class PostgresAppDbContext(DbContextOptions options, IJsonSerializer json
: AppDbContext(options, jsonSerializer) : AppDbContext(options, jsonSerializer)
{ {
public override SqlDialect Dialect => PostgresDialect.Instance; 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 # MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header # SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none dotnet_diagnostic.SA1633.severity = none

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -21,6 +22,7 @@ public sealed class PostgresContentDbContext(string prefix, string connectionStr
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
optionsBuilder.UseBulkInsertPostgreSql();
optionsBuilder.SetDefaultWarnings(); optionsBuilder.SetDefaultWarnings();
optionsBuilder.UseNpgsql(connectionString, options => 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) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropPrimaryKey( migrationBuilder.DropPrimaryKey(
name: "PK_MessagingData", "PK_MessagingData",
table: "MessagingData"); "MessagingData");
migrationBuilder.DropPrimaryKey( migrationBuilder.DropPrimaryKey(
name: "PK_Chats", "PK_Chats",
table: "Chats"); "Chats");
migrationBuilder.DropPrimaryKey( migrationBuilder.DropPrimaryKey(
name: "PK_AssetKeyValueStore_TusMetadata", "PK_AssetKeyValueStore_TusMetadata",
table: "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 # MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header # SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none 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 #nullable disable

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -15,4 +16,10 @@ public sealed class SqlServerAppDbContext(DbContextOptions options, IJsonSeriali
: AppDbContext(options, jsonSerializer) : AppDbContext(options, jsonSerializer)
{ {
public override SqlDialect Dialect => SqlServerDialect.Instance; 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 # MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none dotnet_diagnostic.MA0048.severity = none
# SA1122: Use string.Empty for empty strings
dotnet_diagnostic.SA1122.severity = none
# SA1633: File must have header # SA1633: File must have header
dotnet_diagnostic.SA1633.severity = none dotnet_diagnostic.SA1633.severity = none

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -22,6 +23,7 @@ public sealed class SqlServerContentDbContext(string prefix, string connectionSt
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
optionsBuilder.SetDefaultWarnings(); optionsBuilder.SetDefaultWarnings();
optionsBuilder.UseBulkInsertSqlServer();
optionsBuilder.UseSqlServer(connectionString, options => optionsBuilder.UseSqlServer(connectionString, options =>
{ {
options.MigrationsHistoryTable($"{prefix}MigrationHistory"); 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;
using Squidex.Domain.Apps.Entities.Teams.Repositories; using Squidex.Domain.Apps.Entities.Teams.Repositories;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Events.EntityFramework;
using Squidex.Flows.EntityFramework;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
@ -226,6 +228,9 @@ public static class ServiceExtensions
services.AddSingletonAs<EFUserFactory>() services.AddSingletonAs<EFUserFactory>()
.As<IUserFactory>(); .As<IUserFactory>();
services.AddSingletonAs<BulkInserter>()
.As<IDbEventStoreBulkInserter>().As<IDbFlowsBulkInserter>();
services.AddFlowsCore() services.AddFlowsCore()
.AddEntityFrameworkStore<TContext, FlowEventContext>(); .AddEntityFrameworkStore<TContext, FlowEventContext>();

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

@ -25,29 +25,28 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="8.0.12" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="8.0.16" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.12" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.16" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="8.0.11" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="8.0.11" />
<PackageReference Include="Npgsql.OpenTelemetry" Version="8.0.7" /> <PackageReference Include="Npgsql.OpenTelemetry" Version="8.0.7" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" /> <PackageReference Include="PhenX.EntityFrameworkCore.BulkInsert.MySql" Version="0.2.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.Json.Microsoft" Version="8.0.2" /> <PackageReference Include="PhenX.EntityFrameworkCore.BulkInsert.PostgreSql" Version="0.2.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.NetTopologySuite" Version="8.0.2" /> <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="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.EntityFramework" Version="7.18.0" /> <PackageReference Include="Squidex.AI.EntityFramework" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.EntityFramework" Version="7.18.0" /> <PackageReference Include="Squidex.Assets.EntityFramework" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.18.0" /> <PackageReference Include="Squidex.Assets.TusAdapter" Version="7.22.0" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.Core" Version="8.1.6" /> <PackageReference Include="Squidex.Events.EntityFramework" Version="7.22.0" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.MySql" Version="8.1.6" /> <PackageReference Include="Squidex.Flows.EntityFramework" Version="7.22.0" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.PostgreSQL" Version="8.1.6" /> <PackageReference Include="Squidex.Hosting" Version="7.22.0" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.SqlServer" Version="8.1.6" /> <PackageReference Include="Squidex.Messaging.EntityFramework" Version="7.22.0" />
<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.OpenIdDict.EntityFramework" Version="5.8.4" /> <PackageReference Include="Squidex.OpenIdDict.EntityFramework" Version="5.8.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <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="MongoDB.Driver.GridFS" Version="2.30.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.Mongo" Version="7.18.0" /> <PackageReference Include="Squidex.AI.Mongo" Version="7.22.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="7.18.0" /> <PackageReference Include="Squidex.Assets.Mongo" Version="7.22.0" />
<PackageReference Include="Squidex.Events.Mongo" Version="7.18.0" /> <PackageReference Include="Squidex.Events.Mongo" Version="7.22.0" />
<PackageReference Include="Squidex.Flows.Mongo" Version="7.18.0" /> <PackageReference Include="Squidex.Flows.Mongo" Version="7.22.0" />
<PackageReference Include="Squidex.Hosting" Version="7.18.0" /> <PackageReference Include="Squidex.Hosting" Version="7.22.0" />
<PackageReference Include="Squidex.Messaging.Mongo" Version="7.18.0" /> <PackageReference Include="Squidex.Messaging.Mongo" Version="7.22.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="5.8.4" /> <PackageReference Include="Squidex.OpenIddict.MongoDb" Version="5.8.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <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="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <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="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.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="NJsonSchema" Version="11.0.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI" Version="7.18.0" /> <PackageReference Include="Squidex.AI" Version="7.22.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.18.0" /> <PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.22.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <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="NodaTime" Version="3.2.0" />
<PackageReference Include="OpenTelemetry.Api" Version="1.9.0" /> <PackageReference Include="OpenTelemetry.Api" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="7.18.0" /> <PackageReference Include="Squidex.Assets" Version="7.22.0" />
<PackageReference Include="Squidex.Caching" Version="7.18.0" /> <PackageReference Include="Squidex.Caching" Version="7.22.0" />
<PackageReference Include="Squidex.Events" Version="7.18.0" /> <PackageReference Include="Squidex.Events" Version="7.22.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="7.18.0" /> <PackageReference Include="Squidex.Hosting.Abstractions" Version="7.22.0" />
<PackageReference Include="Squidex.Log" Version="7.18.0" /> <PackageReference Include="Squidex.Log" Version="7.22.0" />
<PackageReference Include="Squidex.Messaging" Version="7.18.0" /> <PackageReference Include="Squidex.Messaging" Version="7.22.0" />
<PackageReference Include="Squidex.Text" Version="7.18.0" /> <PackageReference Include="Squidex.Text" Version="7.22.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" /> <PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.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 var chatRequest = new ChatRequest
{ {
LoadHistory = true,
Configuration = request.Configuration, Configuration = request.Configuration,
ConversationId = request.ConversationId, ConversationId = request.ConversationId,
Prompt = request.Prompt, Prompt = request.Prompt,
@ -105,6 +106,10 @@ public sealed class TranslationsController(ICommandBus commandBus, IAssetStore a
case ToolEndEvent toolEnd: case ToolEndEvent toolEnd:
json = new { type = "ToolEnd", tool = toolEnd.Tool.Spec.DisplayName }; json = new { type = "ToolEnd", tool = toolEnd.Tool.Spec.DisplayName };
break; break;
case ChatHistoryLoaded historyLoaded:
var message = historyLoaded.Message;
json = new { type = "History", content = message.Content, source = message.Type.ToString() };
break;
} }
if (json != null) if (json != null)

24
backend/src/Squidex/Squidex.csproj

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

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

@ -2,27 +2,30 @@
// Auto-generated code // Auto-generated code
namespace Squidex.EntityFramework.TestHelpers; namespace Squidex.EntityFramework.TestHelpers;
[CollectionDefinition("Postgres{{Name}}")] [CollectionDefinition(Name)]
public sealed class Postgres{{Name}}FixtureCollection : ICollectionFixture<Postgres{{Name}}Fixture> 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}}") 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 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}}") 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 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}}") 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}} {{#if HasContentContext}}
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Postgres{{CollectionSuffix}}")] [Collection(Postgres{{CollectionSuffix}}FixtureCollection.Name)]
public class Postgres{{className}}(Postgres{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextPostgres, PostgresContentDbContext>(fixture) public class Postgres{{className}}(Postgres{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextPostgres, PostgresContentDbContext>(fixture)
{ {
} }
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("MySql{{CollectionSuffix}}")] [Collection(MySql{{CollectionSuffix}}FixtureCollection.Name)]
public class MySql{{className}}(MySql{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextMySql, MySqlContentDbContext>(fixture) public class MySql{{className}}(MySql{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextMySql, MySqlContentDbContext>(fixture)
{ {
} }
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("SqlServer{{CollectionSuffix}}")] [Collection(SqlServer{{CollectionSuffix}}FixtureCollection.Name)]
public class SqlServer{{className}}(SqlServer{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextSqlServer, SqlServerContentDbContext>(fixture) public class SqlServer{{className}}(SqlServer{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextSqlServer, SqlServerContentDbContext>(fixture)
{ {
} }
{{else}} {{else}}
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Postgres{{CollectionSuffix}}")] [Collection(Postgres{{CollectionSuffix}}FixtureCollection.Name)]
public class Postgres{{className}}(Postgres{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextPostgres>(fixture) public class Postgres{{className}}(Postgres{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextPostgres>(fixture)
{ {
} }
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("MySql{{CollectionSuffix}}")] [Collection(MySql{{CollectionSuffix}}FixtureCollection.Name)]
public class MySql{{className}}(MySql{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextMySql>(fixture) public class MySql{{className}}(MySql{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextMySql>(fixture)
{ {
} }
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("SqlServer{{CollectionSuffix}}")] [Collection(SqlServer{{CollectionSuffix}}FixtureCollection.Name)]
public class SqlServer{{className}}(SqlServer{{CollectionSuffix}}Fixture fixture) : {{baseName}}<TestDbContextSqlServer>(fixture) 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 public IDbContextNamedFactory<MySqlContentDbContext> DbContextNamedFactory
=> services.GetRequiredService<IDbContextNamedFactory<MySqlContentDbContext>>(); => services.GetRequiredService<IDbContextNamedFactory<MySqlContentDbContext>>();
static MySqlFixture()
{
BulkHelper.Configure();
}
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
await mysql.StartAsync(); 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 public IDbContextNamedFactory<PostgresContentDbContext> DbContextNamedFactory
=> services.GetRequiredService<IDbContextNamedFactory<PostgresContentDbContext>>(); => services.GetRequiredService<IDbContextNamedFactory<PostgresContentDbContext>>();
static PostgresFixture()
{
BulkHelper.Configure();
}
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
await postgreSql.StartAsync(); 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 public IDbContextNamedFactory<SqlServerContentDbContext> DbContextNamedFactory
=> services.GetRequiredService<IDbContextNamedFactory<SqlServerContentDbContext>>(); => services.GetRequiredService<IDbContextNamedFactory<SqlServerContentDbContext>>();
static SqlServerFixture()
{
BulkHelper.Configure();
}
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
await sqlServer.StartAsync(); await sqlServer.StartAsync();

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

@ -6,6 +6,9 @@
// ========================================================================== // ==========================================================================
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PhenX.EntityFrameworkCore.BulkInsert.MySql;
using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
using PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -24,18 +27,36 @@ public class TestDbContextMySql(DbContextOptions options, IJsonSerializer jsonSe
: TestDbContext(options, jsonSerializer) : TestDbContext(options, jsonSerializer)
{ {
public override SqlDialect Dialect => MySqlDialect.Instance; public override SqlDialect Dialect => MySqlDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseBulkInsertMySql();
base.OnConfiguring(optionsBuilder);
}
} }
public class TestDbContextPostgres(DbContextOptions options, IJsonSerializer jsonSerializer) public class TestDbContextPostgres(DbContextOptions options, IJsonSerializer jsonSerializer)
: TestDbContext(options, jsonSerializer) : TestDbContext(options, jsonSerializer)
{ {
public override SqlDialect Dialect => PostgresDialect.Instance; public override SqlDialect Dialect => PostgresDialect.Instance;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseBulkInsertPostgreSql();
base.OnConfiguring(optionsBuilder);
}
} }
public class TestDbContextSqlServer(DbContextOptions options, IJsonSerializer jsonSerializer) public class TestDbContextSqlServer(DbContextOptions options, IJsonSerializer jsonSerializer)
: TestDbContext(options, jsonSerializer) : TestDbContext(options, jsonSerializer)
{ {
public override SqlDialect Dialect => SqlServerDialect.Instance; 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) 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; namespace Squidex.MongoDb.Domain.Apps;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoAppRepositoryTests(MongoFixture fixture) : AppRepositoryTests public class MongoAppRepositoryTests(MongoFixture fixture) : AppRepositoryTests
{ {
protected override async Task<IAppRepository> CreateSutAsync() 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; namespace Squidex.MongoDb.Domain.Assets;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoAssetFolderRepositorySnapshotTests(MongoFixture fixture) : AssetFolderSnapshotStoreTests public class MongoAssetFolderRepositorySnapshotTests(MongoFixture fixture) : AssetFolderSnapshotStoreTests
{ {
protected override async Task<ISnapshotStore<AssetFolder>> CreateSutAsync() 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; namespace Squidex.MongoDb.Domain.Assets;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoAssetRepositorySnapshotTests(MongoFixture fixture) : AssetSnapshotStoreTests public class MongoAssetRepositorySnapshotTests(MongoFixture fixture) : AssetSnapshotStoreTests
{ {
protected override async Task<ISnapshotStore<Asset>> CreateSutAsync() 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; namespace Squidex.MongoDb.Domain.Assets;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoAssetRepositoryTests(MongoFixture fixture) : AssetRepositoryTests public class MongoAssetRepositoryTests(MongoFixture fixture) : AssetRepositoryTests
{ {
protected override async Task<IAssetRepository> CreateSutAsync() 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; namespace Squidex.MongoDb.Domain.Contents;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoContentRepositoryDedicatedTests(MongoFixture fixture) : ContentRepositoryTests public class MongoContentRepositoryDedicatedTests(MongoFixture fixture) : ContentRepositoryTests
{ {
protected override async Task<IContentRepository> CreateSutAsync() 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; namespace Squidex.MongoDb.Domain.Contents;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoContentRepositorySnapshotDedicatedTests(MongoFixture fixture) : ContentSnapshotStoreTests public class MongoContentRepositorySnapshotDedicatedTests(MongoFixture fixture) : ContentSnapshotStoreTests
{ {
protected override bool CheckConsistencyOnWrite => false; 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; namespace Squidex.MongoDb.Domain.Contents;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoContentRepositorySnapshotTests(MongoFixture fixture) : ContentSnapshotStoreTests public class MongoContentRepositorySnapshotTests(MongoFixture fixture) : ContentSnapshotStoreTests
{ {
protected override bool CheckConsistencyOnWrite => false; 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; namespace Squidex.MongoDb.Domain.Contents;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoContentRepositoryTests(MongoFixture fixture) : ContentRepositoryTests public class MongoContentRepositoryTests(MongoFixture fixture) : ContentRepositoryTests
{ {
protected override async Task<IContentRepository> CreateSutAsync() 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; namespace Squidex.MongoDb.Domain.Contents.Text;
[Trait("Category", "Dependencies")] [Trait("Category", "Dependencies")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoTextIndexTests(MongoFixture fixture) : TextIndexerTests public class MongoTextIndexTests(MongoFixture fixture) : TextIndexerTests
{ {
public override bool SupportsQuerySyntax => false; 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; namespace Squidex.MongoDb.Domain.Contents.Text;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoTextIndexerStateTests(MongoFixture fixture) : TextIndexerStateTests public class MongoTextIndexerStateTests(MongoFixture fixture) : TextIndexerStateTests
{ {
protected override async Task<ITextIndexerState> CreateSutAsync(IContentRepository contentRepository) 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; namespace Squidex.MongoDb.Domain.History;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoHistoryEventRepositoryTests(MongoFixture fixture) : HistoryEventRepositoryTests public class MongoHistoryEventRepositoryTests(MongoFixture fixture) : HistoryEventRepositoryTests
{ {
protected override async Task<IHistoryEventRepository> CreateSutAsync() 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; namespace Squidex.MongoDb.Domain.Rules;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoRuleRepositoryTests(MongoFixture fixture) : RuleRepositoryTests public class MongoRuleRepositoryTests(MongoFixture fixture) : RuleRepositoryTests
{ {
protected override async Task<IRuleRepository> CreateSutAsync() 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; namespace Squidex.MongoDb.Domain.Schemas;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoSchemaRepositoryTests(MongoFixture fixture) : SchemaRepositoryTests public class MongoSchemaRepositoryTests(MongoFixture fixture) : SchemaRepositoryTests
{ {
protected override async Task<ISchemaRepository> CreateSutAsync() 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; namespace Squidex.MongoDb.Domain.Schemas;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoSchemasHashTests(MongoFixture fixture) : GivenContext, IAsyncLifetime public class MongoSchemasHashTests(MongoFixture fixture) : GivenContext, IAsyncLifetime
{ {
private readonly MongoSchemasHash sut = new MongoSchemasHash(fixture.Database); 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; namespace Squidex.MongoDb.Domain.Teams;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoTeamRepositoryTests(MongoFixture fixture) : TeamRepositoryTests public class MongoTeamRepositoryTests(MongoFixture fixture) : TeamRepositoryTests
{ {
protected override async Task<ITeamRepository> CreateSutAsync() 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; namespace Squidex.MongoDb.Infrastructure.Caching;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoDistributedCacheTests(MongoFixture fixture) : DistributedCacheTests public class MongoDistributedCacheTests(MongoFixture fixture) : DistributedCacheTests
{ {
protected override async Task<IDistributedCache> CreateSutAsync(TimeProvider timeProvider) 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; namespace Squidex.MongoDb.Infrastructure.Log;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoRequestLogRepositoryTests(MongoFixture fixture) : RequestLogRepositoryTests public class MongoRequestLogRepositoryTests(MongoFixture fixture) : RequestLogRepositoryTests
{ {
protected override async Task<IRequestLogRepository> CreateSutAsync() 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; namespace Squidex.MongoDb.Infrastructure.Migrations;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoMigrationStatusTests(MongoFixture fixture) : MigrationStatusTests public class MongoMigrationStatusTests(MongoFixture fixture) : MigrationStatusTests
{ {
protected override async Task<IMigrationStatus> CreateSutAsync() 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; namespace Squidex.MongoDb.Infrastructure.States;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoSnapshotStoreTests(MongoFixture fixture) : SnapshotStoreTests public class MongoSnapshotStoreTests(MongoFixture fixture) : SnapshotStoreTests
{ {
protected override async Task<ISnapshotStore<SnapshotValue>> CreateSutAsync() 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; namespace Squidex.MongoDb.Infrastructure.UsageTracking;
[Trait("Category", "TestContainer")] [Trait("Category", "TestContainer")]
[Collection("Mongo")] [Collection(MongoFixtureCollection.Name)]
public class MongoUsageRepositoryTests(MongoFixture fixture) : UsageRepositoryTests public class MongoUsageRepositoryTests(MongoFixture fixture) : UsageRepositoryTests
{ {
protected override async Task<IUsageRepository> CreateSutAsync() 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; namespace Squidex.MongoDb.TestHelpers;
[CollectionDefinition("Mongo")] [CollectionDefinition(Name)]
public sealed class MongoFixtureCollection : ICollectionFixture<MongoFixture> public sealed class MongoFixtureCollection : ICollectionFixture<MongoFixture>
{ {
public const string Name = "Mongo";
} }
public class MongoFixture : IAsyncLifetime 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) 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)); var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected));
AssertIds(actual, 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)); var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected));
AssertIds(actual, 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)); var actual = await SearchAsync(i => i.SearchAsync(App, query, target, default), x => IsExpected(x, expected));
AssertIds(actual, 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> {{ "tour.welcome" | sqxTranslate }} <span class="header-focus">{{ "tour.welcomeProduct" | sqxTranslate }}</span>
</h1> </h1>
<div [sqxMarkdown]="'tour.stepIntroText' | sqxTranslate"></div> <div inline="false" [sqxMarkdown]="'tour.stepIntroText' | sqxTranslate"></div>
<div class="mt-4"> <div class="mt-4">
<button class="btn btn-success" (click)="next()">{{ "tour.stepIntroNext" | sqxTranslate }}</button> <button class="btn btn-success" (click)="next()">{{ "tour.stepIntroNext" | sqxTranslate }}</button>
@ -34,15 +34,10 @@
<label for="role">{{ "tour.stepDataCompanyRole" | sqxTranslate }}</label> <label for="role">{{ "tour.stepDataCompanyRole" | sqxTranslate }}</label>
<select class="form-select" id="companyRole" formControlName="companyRole"> <select class="form-select" id="companyRole" formControlName="companyRole">
<option [ngValue]="'RoleEmployee'">{{ "tour.roleEmployee" | sqxTranslate }}</option> <option [ngValue]="'RoleEmployee'">{{ "tour.roleEmployee" | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessOwner'">{{ "tour.roleBusinessOwner" | sqxTranslate }}</option> <option [ngValue]="'RoleBusinessOwner'">{{ "tour.roleBusinessOwner" | sqxTranslate }}</option>
<option [ngValue]="'RoleProductManager'">{{ "tour.roleProductManager" | sqxTranslate }}</option> <option [ngValue]="'RoleProductManager'">{{ "tour.roleProductManager" | sqxTranslate }}</option>
<option [ngValue]="'RoleContentCreator'">{{ "tour.roleContentCreator" | sqxTranslate }}</option> <option [ngValue]="'RoleContentCreator'">{{ "tour.roleContentCreator" | sqxTranslate }}</option>
<option [ngValue]="'RoleSoftwareDeveloper'">{{ "tour.roleSoftwareDeveloper" | sqxTranslate }}</option> <option [ngValue]="'RoleSoftwareDeveloper'">{{ "tour.roleSoftwareDeveloper" | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessAnalyst'">{{ "tour.roleBusinessAnalyst" | sqxTranslate }}</option> <option [ngValue]="'RoleBusinessAnalyst'">{{ "tour.roleBusinessAnalyst" | sqxTranslate }}</option>
</select> </select>
</div> </div>
@ -51,13 +46,9 @@
<label for="companySize">{{ "tour.stepDataCompanySize" | sqxTranslate }}</label> <label for="companySize">{{ "tour.stepDataCompanySize" | sqxTranslate }}</label>
<select class="form-select" id="companySize" formControlName="companySize"> <select class="form-select" id="companySize" formControlName="companySize">
<option [ngValue]="'SizeSingle'">{{ "tour.sizeSingle" | sqxTranslate }}</option> <option [ngValue]="'SizeSingle'">{{ "tour.sizeSingle" | sqxTranslate }}</option>
<option [ngValue]="'SizeSmall'">{{ "tour.sizeSmall" | sqxTranslate }}</option> <option [ngValue]="'SizeSmall'">{{ "tour.sizeSmall" | sqxTranslate }}</option>
<option [ngValue]="'SizeMedium'">{{ "tour.sizeMedium" | sqxTranslate }}</option> <option [ngValue]="'SizeMedium'">{{ "tour.sizeMedium" | sqxTranslate }}</option>
<option [ngValue]="'SizeLarge'">{{ "tour.sizeLarge" | sqxTranslate }}</option> <option [ngValue]="'SizeLarge'">{{ "tour.sizeLarge" | sqxTranslate }}</option>
<option [ngValue]="'SizeVeryLarge'">{{ "tour.sizeVeryLarge" | sqxTranslate }}</option> <option [ngValue]="'SizeVeryLarge'">{{ "tour.sizeVeryLarge" | sqxTranslate }}</option>
</select> </select>
</div> </div>
@ -66,17 +57,11 @@
<label for="project">{{ "tour.stepDataProject" | sqxTranslate }}</label> <label for="project">{{ "tour.stepDataProject" | sqxTranslate }}</label>
<select class="form-select" id="project" formControlName="project"> <select class="form-select" id="project" formControlName="project">
<option [ngValue]="'ProjectNewsMagazine'">{{ "tour.projectNewsMagazine" | sqxTranslate }}</option> <option [ngValue]="'ProjectNewsMagazine'">{{ "tour.projectNewsMagazine" | sqxTranslate }}</option>
<option [ngValue]="'ProjectPersonalBlog'">{{ "tour.projectPersonalBlog" | sqxTranslate }}</option> <option [ngValue]="'ProjectPersonalBlog'">{{ "tour.projectPersonalBlog" | sqxTranslate }}</option>
<option [ngValue]="'ProjectSmallBusiness'">{{ "tour.projectSmallBusiness" | sqxTranslate }}</option> <option [ngValue]="'ProjectSmallBusiness'">{{ "tour.projectSmallBusiness" | sqxTranslate }}</option>
<option [ngValue]="'ProjectCommerce'">{{ "tour.projectCommerce" | sqxTranslate }}</option> <option [ngValue]="'ProjectCommerce'">{{ "tour.projectCommerce" | sqxTranslate }}</option>
<option [ngValue]="'ProjectMobileApp'">{{ "tour.projectMobileApp" | sqxTranslate }}</option> <option [ngValue]="'ProjectMobileApp'">{{ "tour.projectMobileApp" | sqxTranslate }}</option>
<option [ngValue]="'ProjectBackend'">{{ "tour.projectBackend" | sqxTranslate }}</option> <option [ngValue]="'ProjectBackend'">{{ "tour.projectBackend" | sqxTranslate }}</option>
<option [ngValue]="'ProjectLearning'">{{ "tour.projectLearning" | sqxTranslate }}</option> <option [ngValue]="'ProjectLearning'">{{ "tour.projectLearning" | sqxTranslate }}</option>
</select> </select>
</div> </div>

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

@ -62,7 +62,7 @@
replaceUrl="true" replaceUrl="true"
routerLink="history" routerLink="history"
routerLinkActive="active" routerLinkActive="active"
sqxTourStep="help" sqxTourStep="history"
title="i18n:common.history" title="i18n:common.history"
titlePosition="left"> titlePosition="left">
<i class="icon-time"></i> <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 { export class TourStepDirective implements OnInit, OnDestroy, TourAnchorDirective {
private isNextOnClick = false; private isNextOnClick = false;
private isActive = false; private isActive = false;
private currentAnchorId?: string | null;
private wasClicked = false; private wasClicked = false;
@Input({ alias: 'sqxTourStep', required: true }) @Input({ alias: 'sqxTourStep', required: true })
@ -27,28 +28,30 @@ export class TourStepDirective implements OnInit, OnDestroy, TourAnchorDirective
} }
public ngOnInit(): void { public ngOnInit(): void {
if (!this.anchorId) { this.currentAnchorId = this.anchorId;
if (!this.currentAnchorId) {
return; return;
} }
this.tourService.register(this.anchorId, this); this.tourService.register(this.currentAnchorId, this);
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
if (!this.anchorId) { if (!this.currentAnchorId) {
return; return;
} }
if (this.isActive && (!this.isNextOnClick || !this.wasClicked)) { if (this.isActive && (!this.isNextOnClick || !this.wasClicked)) {
setTimeout(() => { setTimeout(() => {
if (this.tourService.currentStep.anchorId === this.anchorId) { if (this.tourService.currentStep.anchorId === this.currentAnchorId) {
this.tourService.render(null, null); this.tourService.render(null, null);
this.tourService.pause(); this.tourService.pause();
} }
}, 200); }, 200);
} }
this.tourService.unregister(this.anchorId); this.tourService.unregister(this.currentAnchorId);
} }
@HostListener('click') @HostListener('click')

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

@ -19,7 +19,7 @@
<div class="col"> <div class="col">
<h5 inline="true" [sqxMarkdown]="currentStep.title | sqxTranslate"></h5> <h5 inline="true" [sqxMarkdown]="currentStep.title | sqxTranslate"></h5>
<div [sqxMarkdown]="currentStep.content | sqxTranslate"></div> <div [sqxMarkdown]="currentStep.content | sqxTranslate" inline="false"></div>
</div> </div>
<div class="col-auto"><button class="btn btn-sm btn-close" (click)="tourService.end()" type="button"></button></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 { 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 { filter, Observable, Subscription, take } from 'rxjs';
import { FloatingPlacement } from '@app/framework/internal'; import { FloatingPlacement } from '@app/framework/internal';
import { TourTemplateComponent } from './tour-template.component'; import { TourTemplateComponent } from './tour-template.component';
@ -21,8 +21,11 @@ export interface StepDefinition extends IStepOption {
// Additional callback. // Additional callback.
hideThis?: () => void; hideThis?: () => void;
// Goes to the end automatically.
endOnCondition?: ((service: TourService, anchor: TourAnchorDirective) => Observable<any>) | null;
// Goes to the next element automatically. // 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) { 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({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class TourService extends BaseTourService<StepDefinition> { export class TourService extends BaseTourService<StepDefinition> {
private condition?: Subscription; private onNext?: Subscription;
private onEnd?: Subscription;
public component?: TourTemplateComponent | null = null; public component?: TourTemplateComponent | null = null;
@ -55,10 +116,24 @@ export class TourService extends BaseTourService<StepDefinition> {
document.body.style.overflow = 'auto'; 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$ this.stepHide$
.subscribe(() => { .subscribe(() => {
if (this.getStatus() !== TourState.PAUSED) { 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) { public render(step: StepDefinition | null, target: any | null) {
this.component?.render(step, target); 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 { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms'; 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 { HTTP, MathHelper, ModalDialogComponent, TooltipDirective, TranslatePipe } from '@app/framework';
import { AppsState, AuthService, ChatEventDto, StatefulComponent, TranslationsService } from '@app/shared/internal'; import { AppsState, AuthService, ChatEventDto, StatefulComponent, TranslationsService } from '@app/shared/internal';
import { ChatItemComponent } from './chat-item.component'; import { ChatItemComponent } from './chat-item.component';
@ -21,7 +21,7 @@ interface State {
isRunning: boolean; isRunning: boolean;
// The answers. // The answers.
chatItems: ReadonlyArray<{ content: string | Observable<ChatEventDto>; type: 'User' | 'Bot' | 'System' }>; chatItems: ReadonlyArray<{ content: string | Observable<ChatEventDto>; type: 'User' | 'Assistant' | 'System' }>;
} }
@Component({ @Component({
@ -38,14 +38,15 @@ interface State {
], ],
}) })
export class ChatDialogComponent extends StatefulComponent<State> { export class ChatDialogComponent extends StatefulComponent<State> {
private readonly conversationId = MathHelper.guid();
@Output() @Output()
public contentSelect = new EventEmitter<string | HTTP.UploadFile | undefined | null>(); public contentSelect = new EventEmitter<string | HTTP.UploadFile | undefined | null>();
@Input() @Input()
public configuration?: string; public configuration?: string;
@Input()
public conversationId = MathHelper.guid();
@Input() @Input()
public folderId?: string; public folderId?: string;
@ -71,14 +72,32 @@ export class ChatDialogComponent extends StatefulComponent<State> {
public ngOnInit() { public ngOnInit() {
const { configuration, conversationId } = this; const { configuration, conversationId } = this;
const stream = this.translator.ask(this.appsState.appName, { conversationId, configuration });
this.next(s => ({ let observable: Subject<ChatEventDto>;
...s, this.translator.ask(this.appsState.appName, { conversationId, configuration })
chatQuestion: '', .subscribe(message => {
chatItems: [...s.chatItems, { content: stream, type: 'Bot' }], if (message.type === 'History') {
isRunning: true, 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) { public setQuestion(chatQuestion: string) {
@ -105,7 +124,7 @@ export class ChatDialogComponent extends StatefulComponent<State> {
chatItems: [ chatItems: [
...s.chatItems, ...s.chatItems,
{ content: prompt, type: 'User' }, { content: prompt, type: 'User' },
{ content: stream, type: 'Bot' }, { content: stream, type: 'Assistant' },
], ],
isRunning: true, isRunning: true,
})); }));

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

@ -20,7 +20,7 @@
</div> </div>
} }
@if (type === "Bot") { @if (type === "Assistant") {
<div class="row mt-3"> <div class="row mt-3">
<div class="col-auto"> <div class="col-auto">
<div class="squid squid-sm d-flex align-items-center justify-content-center"><img src="./images/squid.svg" /></div> <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> <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"> <button class="btn btn-secondary btn-sm btn-text" (click)="selectContent()" [disabled]="snapshot.isCopying" type="button">
{{ "chat.use" | sqxTranslate }} {{ "chat.use" | sqxTranslate }}
@if (snapshot.isCopying) { @if (snapshot.isCopying) {
@ -55,12 +55,10 @@
} }
</div> </div>
@if (snapshot.isRunning) { @if (snapshot.isRunning && !snapshot.content) {
<svg class="loader" height="10" viewBox="0 0 40 16"> <svg class="loader" height="10" viewBox="0 0 40 16">
<circle class="dot" cx="8" cy="8" r="4" /> <circle class="dot" cx="8" cy="8" r="4" />
<circle class="dot" cx="20" cy="8" r="4" /> <circle class="dot" cx="20" cy="8" r="4" />
<circle class="dot" cx="32" cy="8" r="4" /> <circle class="dot" cx="32" cy="8" r="4" />
</svg> </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>; public focusElement!: ElementRef<HTMLElement>;
@Input({ required: true }) @Input({ required: true })
public type: 'Bot' | 'User' | 'System' = 'Bot'; public type: 'Assistant' | 'User' | 'System' = 'Assistant';
@Input({ required: true }) @Input({ required: true })
public folderId?: string; 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"> <a href="https://squidex.io/help" target="_blank">
<div class="row g-0 align-items-center"> <div class="row g-0 align-items-center">
<div class="col-auto"><i class="icon-help"></i></div> <div class="col-auto"><i class="icon-help"></i></div>
<div class="col ps-4">{{ "tour.documentation" | sqxTranslate }}</div> <div class="col ps-4">{{ "tour.documentation" | sqxTranslate }}</div>
<div class="col-auto"><i class="icon-angle-right"></i></div> <div class="col-auto"><i class="icon-angle-right"></i></div>
</div> </div>
</a> </a>
<a href="https://support.squidex.io" target="_blank"> <a href="https://support.squidex.io" target="_blank">
<div class="row g-0 align-items-center"> <div class="row g-0 align-items-center">
<div class="col-auto"><i class="icon-user-o"></i></div> <div class="col-auto"><i class="icon-user-o"></i></div>
<div class="col ps-4">{{ "tour.support" | sqxTranslate }}</div> <div class="col ps-4">{{ "tour.support" | sqxTranslate }}</div>
<div class="col-auto"><i class="icon-angle-right"></i></div> <div class="col-auto"><i class="icon-angle-right"></i></div>
</div> </div>
</a> </a>

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

@ -77,6 +77,7 @@ $caret-size: 14px;
.summary { .summary {
align-items: center; align-items: center;
color: $color-white; color: $color-white;
cursor: pointer;
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; 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) { public start(task: TaskSnapshot) {
if (!task.isActive) {
return;
}
this.tourState.runTask(task); 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); 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')); pretifyError('i18n:rules.triggerFailed'));
} }

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

@ -31,6 +31,16 @@ export interface ChatChunkDto {
content: string; 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 { export interface ChatToolStartDto {
type: 'ToolStart'; type: 'ToolStart';
@ -45,7 +55,7 @@ export interface ChatToolEndDto {
tool: string; tool: string;
} }
export type ChatEventDto = ChatChunkDto | ChatToolStartDto | ChatToolEndDto; export type ChatEventDto = ChatChunkDto | ChatToolStartDto | ChatToolEndDto | ChatHistoryLoaded;
@Injectable({ @Injectable({

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

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

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

@ -7,7 +7,7 @@
import { inject, InjectionToken } from '@angular/core'; import { inject, InjectionToken } from '@angular/core';
import { filter, Observable, take } from 'rxjs'; 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 { ClientTourStated, QueryExecuted } from '../utils/messages';
import { AppsState } from './apps.state'; import { AppsState } from './apps.state';
import { AssetsState } from './assets.state'; import { AssetsState } from './assets.state';
@ -128,11 +128,13 @@ export function buildTasks() {
}, { }, {
anchorId: 'help', anchorId: 'help',
content: 'i18n:tour.createSchema.helpContent', content: 'i18n:tour.createSchema.helpContent',
endOnCondition: waitForElement('.panel2.minimized .right'),
scrollContainer: '.panel-container', scrollContainer: '.panel-container',
position: 'left-start', position: 'left-start',
}, { }, {
anchorId: 'history', anchorId: 'history',
content: 'i18n:tour.createSchema.historyContent', content: 'i18n:tour.createSchema.historyContent',
endOnCondition: waitForElement('.panel2.minimized .right'),
scrollContainer: '.panel-container', scrollContainer: '.panel-container',
position: 'left-start', position: 'left-start',
}], }],
@ -183,6 +185,8 @@ export function buildTasks() {
}, { }, {
anchorId: 'status', anchorId: 'status',
content: 'i18n:tour.createContent.statusContent', content: 'i18n:tour.createContent.statusContent',
endOnCondition: waitForAnchorClick(),
nextOnAnchorClick: true,
isAsync: true, isAsync: true,
position: 'left-start', position: 'left-start',
}], }],
@ -221,6 +225,7 @@ export function buildTasks() {
anchorId: 'filter', anchorId: 'filter',
content: 'i18n:tour.createAsset.filterContent', content: 'i18n:tour.createAsset.filterContent',
position: 'left-start', position: 'left-start',
endOnCondition: waitForElement('.panel2.minimized .right'),
}], }],
onComplete: (() => { onComplete: (() => {
const assetsState = inject(AssetsState); 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> <li class="nav-item nav-icon" (click)="chatDialog.show()"><span class="nav-link">AI</span></li>
</ul> </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 { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 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({ @Component({
standalone: true, standalone: true,
@ -28,6 +28,7 @@ export class ChatMenuComponent {
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,
public readonly authService: AuthService,
) { ) {
} }
} }

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

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

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

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

Loading…
Cancel
Save