Browse Source

Use auto generated code (#1213)

* Improve tests.

Fix

# Conflicts:
#	backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs
#	backend/src/Squidex.Data.MongoDb/ServiceExtensions.cs

* Fix all tests

* Add missing files.

* More stuff.

* Fix workflows stuff.

* Fix state.

* Temp

* Build fix.

* Fixes
pull/1214/head
Sebastian Stehle 1 year ago
committed by GitHub
parent
commit
64f411a89b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 30
      backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs
  2. 3
      backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs
  3. 1
      backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs
  4. 31
      backend/src/Squidex/Areas/Api/Config/OpenApi/TagXmlProcessor.cs
  5. 6
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs
  6. 18
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs
  7. 2
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs
  8. 2
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/EnrichContentDefaultsDto.cs
  9. 27
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicCreateRuleDto.cs
  10. 81
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRuleDto.cs
  11. 24
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRulesDto.cs
  12. 34
      backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicUpdateRuleDto.cs
  13. 6
      backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs
  14. 8
      backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  15. 1
      backend/src/Squidex/Startup.cs
  16. 22
      frontend/generator/Generator.sln
  17. 30
      frontend/generator/Generator/Generator.csproj
  18. 165
      frontend/generator/Generator/Program.cs
  19. 205
      frontend/generator/Generator/Templates/Class.liquid
  20. 65
      frontend/generator/Generator/Templates/ConvertToClass.liquid
  21. 45
      frontend/generator/Generator/Templates/ConvertToJavaScript.liquid
  22. 10
      frontend/generator/Generator/Templates/Enum.StringLiteral.liquid
  23. 4
      frontend/src/app/features/administration/pages/event-consumers/event-consumer.component.ts
  24. 4
      frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  25. 25
      frontend/src/app/features/administration/pages/restore/restore-page.component.ts
  26. 51
      frontend/src/app/features/administration/pages/users/user-page.component.ts
  27. 60
      frontend/src/app/features/administration/services/event-consumers.service.spec.ts
  28. 61
      frontend/src/app/features/administration/services/event-consumers.service.ts
  29. 100
      frontend/src/app/features/administration/services/users.service.spec.ts
  30. 99
      frontend/src/app/features/administration/services/users.service.ts
  31. 8
      frontend/src/app/features/administration/state/event-consumers.state.spec.ts
  32. 4
      frontend/src/app/features/administration/state/event-consumers.state.ts
  33. 7
      frontend/src/app/features/administration/state/users.forms.ts
  34. 32
      frontend/src/app/features/administration/state/users.state.spec.ts
  35. 10
      frontend/src/app/features/administration/state/users.state.ts
  36. 1
      frontend/src/app/features/assets/pages/asset-tag-dialog.component.ts
  37. 4
      frontend/src/app/features/content/pages/calendar/calendar-page.component.ts
  38. 6
      frontend/src/app/features/content/pages/content/content-event.component.ts
  39. 5
      frontend/src/app/features/content/pages/content/content-page.component.ts
  40. 4
      frontend/src/app/features/content/pages/contents/contents-page.component.ts
  41. 12
      frontend/src/app/features/content/shared/due-time-selector.component.ts
  42. 2
      frontend/src/app/features/content/shared/forms/array-editor.component.ts
  43. 6
      frontend/src/app/features/content/shared/forms/array-item.component.ts
  44. 8
      frontend/src/app/features/content/shared/forms/content-field.component.ts
  45. 4
      frontend/src/app/features/content/shared/forms/content-section.component.ts
  46. 21
      frontend/src/app/features/content/shared/forms/field-editor.component.ts
  47. 27
      frontend/src/app/features/content/shared/list/content.component.ts
  48. 36
      frontend/src/app/features/content/shared/references/content-creator.component.ts
  49. 6
      frontend/src/app/features/content/shared/references/reference-dropdown.component.ts
  50. 6
      frontend/src/app/features/content/shared/references/reference-item.component.ts
  51. 4
      frontend/src/app/features/content/shared/references/references-checkboxes.component.ts
  52. 4
      frontend/src/app/features/content/shared/references/references-radio-buttons.component.ts
  53. 6
      frontend/src/app/features/content/shared/references/references-tag-converter.ts
  54. 6
      frontend/src/app/features/content/shared/references/references-tags.component.ts
  55. 4
      frontend/src/app/features/rules/pages/rule/rule-page.component.html
  56. 24
      frontend/src/app/features/rules/pages/rule/rule-page.component.ts
  57. 6
      frontend/src/app/features/rules/pages/rules/rule.component.html
  58. 14
      frontend/src/app/features/rules/pages/rules/rule.component.ts
  59. 8
      frontend/src/app/features/rules/pages/rules/rules-page.component.ts
  60. 2
      frontend/src/app/features/rules/shared/actions/generic-action.component.html
  61. 23
      frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.ts
  62. 23
      frontend/src/app/features/schemas/pages/schema/export/schema-export-form.component.ts
  63. 8
      frontend/src/app/features/schemas/pages/schema/fields/field-group.component.ts
  64. 2
      frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html
  65. 21
      frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.ts
  66. 35
      frontend/src/app/features/schemas/pages/schema/fields/field.component.ts
  67. 4
      frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts
  68. 4
      frontend/src/app/features/schemas/pages/schema/fields/forms/field-form.component.ts
  69. 6
      frontend/src/app/features/schemas/pages/schema/fields/sortable-field-list.component.ts
  70. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts
  71. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts
  72. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts
  73. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts
  74. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts
  75. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.ts
  76. 6
      frontend/src/app/features/schemas/pages/schema/fields/types/number-validation.component.ts
  77. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/references-ui.component.ts
  78. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.ts
  79. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts
  80. 8
      frontend/src/app/features/schemas/pages/schema/fields/types/string-validation.component.ts
  81. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts
  82. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/tags-validation.component.ts
  83. 25
      frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.ts
  84. 23
      frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts
  85. 25
      frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts
  86. 23
      frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts
  87. 2
      frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts
  88. 14
      frontend/src/app/features/schemas/pages/schema/ui/schema-ui-form.component.ts
  89. 23
      frontend/src/app/features/schemas/pages/schemas/schema-form.component.ts
  90. 13
      frontend/src/app/features/schemas/pages/schemas/schemas-page.component.ts
  91. 23
      frontend/src/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts
  92. 23
      frontend/src/app/features/settings/pages/clients/client-add-form.component.ts
  93. 2
      frontend/src/app/features/settings/pages/clients/client.component.html
  94. 20
      frontend/src/app/features/settings/pages/clients/client.component.ts
  95. 33
      frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.ts
  96. 6
      frontend/src/app/features/settings/pages/contributors/contributor.component.ts
  97. 4
      frontend/src/app/features/settings/pages/contributors/import-contributors-dialog.component.ts
  98. 23
      frontend/src/app/features/settings/pages/languages/language-add-form.component.ts
  99. 31
      frontend/src/app/features/settings/pages/languages/language.component.ts
  100. 54
      frontend/src/app/features/settings/pages/more/more-page.component.ts

30
backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs

@ -12,6 +12,8 @@ using NJsonSchema.Generation.TypeMappers;
using NodaTime;
using NSwag.Generation;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
@ -88,6 +90,9 @@ public static class OpenApiServices
services.AddOpenApiDocument((settings, services) =>
{
ConfigureSchemaSettings(settings.SchemaSettings, services.GetRequiredService<TypeRegistry>(), false);
settings.DocumentProcessors.Add(new AddAdditionalTypeProcessor<DynamicCreateRuleDto>());
settings.DocumentProcessors.Add(new AddAdditionalTypeProcessor<DynamicRulesDto>());
settings.DocumentProcessors.Add(new AddAdditionalTypeProcessor<DynamicUpdateRuleDto>());
});
}
@ -103,8 +108,8 @@ public static class OpenApiServices
settings.SchemaProcessors.Add(new RequiredSchemaProcessor());
settings.SchemaType = NJsonSchema.SchemaType.OpenApi3;
settings.TypeMappers = new List<ITypeMapper>
{
settings.TypeMappers =
[
CreateAnyMap<FilterNode<JsonValue>>(),
CreateAnyMap<JsonDocument>(),
CreateAnyMap<JsonValue>(),
@ -123,15 +128,14 @@ public static class OpenApiServices
CreateStringMap<PropertyPath>(),
CreateStringMap<RefToken>(),
CreateStringMap<Status>(),
};
];
}
private static ITypeMapper CreateObjectMap<T>()
private static PrimitiveTypeMapper CreateObjectMap<T>()
{
return new PrimitiveTypeMapper(typeof(T), schema =>
{
schema.Type = JsonObjectType.Object;
schema.AdditionalPropertiesSchema = new JsonSchema
{
Description = "Any",
@ -139,12 +143,11 @@ public static class OpenApiServices
});
}
private static ITypeMapper CreateArrayMap<T>(JsonObjectType itemType)
private static PrimitiveTypeMapper CreateArrayMap<T>(JsonObjectType itemType)
{
return new PrimitiveTypeMapper(typeof(T), schema =>
{
schema.Type = JsonObjectType.Array;
schema.Item = new JsonSchema
{
Type = itemType,
@ -152,21 +155,28 @@ public static class OpenApiServices
});
}
private static ITypeMapper CreateStringMap<T>(string? format = null)
private static PrimitiveTypeMapper CreateStringMap<T>(string? format = null)
{
return new PrimitiveTypeMapper(typeof(T), schema =>
{
schema.Type = JsonObjectType.String;
schema.Format = format;
});
}
private static ITypeMapper CreateAnyMap<T>()
private static PrimitiveTypeMapper CreateAnyMap<T>()
{
return new PrimitiveTypeMapper(typeof(T), schema =>
{
schema.Type = JsonObjectType.None;
});
}
public sealed class AddAdditionalTypeProcessor<T> : IDocumentProcessor where T : class
{
public void Process(DocumentProcessorContext context)
{
context.SchemaResolver.AppendSchema(context.SchemaGenerator.Generate(typeof(T), context.SchemaResolver), null);
}
}
}

3
backend/src/Squidex/Areas/Api/Config/OpenApi/ScopesProcessor.cs

@ -18,10 +18,9 @@ public sealed class ScopesProcessor : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>();
context.OperationDescription.Operation.Security ??= [];
var permissionAttribute = context.MethodInfo.GetCustomAttribute<ApiPermissionAttribute>();
if (permissionAttribute != null)
{
context.OperationDescription.Operation.Security.Add(new OpenApiSecurityRequirement

1
backend/src/Squidex/Areas/Api/Config/OpenApi/TagByGroupNameProcessor.cs

@ -21,7 +21,6 @@ public sealed class TagByGroupNameProcessor : IOperationProcessor
if (!string.IsNullOrWhiteSpace(groupName))
{
context.OperationDescription.Operation.Tags = [groupName];
return true;
}
else

31
backend/src/Squidex/Areas/Api/Config/OpenApi/TagXmlProcessor.cs

@ -22,25 +22,28 @@ public sealed class TagXmlProcessor : IDocumentProcessor
foreach (var controllerType in context.ControllerTypes)
{
var attribute = controllerType.GetCustomAttribute<ApiExplorerSettingsAttribute>();
if (attribute == null)
{
continue;
}
if (attribute != null)
var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName);
if (tag == null)
{
var tag = context.Document.Tags.FirstOrDefault(x => x.Name == attribute.GroupName);
continue;
}
if (tag != null)
{
var description = controllerType.GetXmlDocsSummary();
var description = controllerType.GetXmlDocsSummary();
if (description == null)
{
continue;
}
if (description != null)
{
tag.Description ??= string.Empty;
tag.Description ??= string.Empty;
if (!tag.Description.Contains(description, StringComparison.Ordinal))
{
tag.Description += "\n\n" + description;
}
}
}
if (!tag.Description.Contains(description, StringComparison.Ordinal))
{
tag.Description += "\n\n" + description;
}
}
}

6
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs

@ -138,13 +138,13 @@ internal sealed class OperationBuilder(OperationsBuilder operations, OpenApiOper
{
var fullId = PermissionIds.ForApp(permissionId, operations.Parent.AppName, operations.SchemaName).Id;
operation.Security = new List<OpenApiSecurityRequirement>
{
operation.Security =
[
new OpenApiSecurityRequirement
{
[Constants.SecurityDefinition] = [fullId],
},
};
];
return this;
}

18
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs

@ -221,18 +221,18 @@ public sealed class SchemasOpenApiGenerator(
var document = new OpenApiDocument
{
Schemes = new List<OpenApiSchema>
{
Schemes =
[
scheme,
},
Consumes = new List<string>
{
],
Consumes =
[
"application/json",
},
Produces = new List<string>
{
],
Produces =
[
"application/json",
},
],
Info = new OpenApiInfo
{
Title = $"Squidex Content API for '{appName}' App",

2
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs

@ -34,7 +34,7 @@ public sealed class BulkUpdateContentsDto
public bool DoNotScript { get; set; } = true;
/// <summary>
/// True, to also enrich required fields. Default: false.
/// True, to also enrich required fields. Default: false.
/// </summary>
public bool EnrichRequiredFields { get; set; }

2
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/EnrichContentDefaultsDto.cs

@ -15,7 +15,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models;
public class EnrichContentDefaultsDto
{
/// <summary>
/// True, to also enrich required fields. Default: false.
/// True, to also enrich required fields. Default: false.
/// </summary>
[FromQuery(Name = "enrichRequiredFields")]
public bool EnrichRequiredFields { get; set; }

27
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicCreateRuleDto.cs

@ -0,0 +1,27 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Validation;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models;
[OpenApiRequest]
public sealed class DynamicCreateRuleDto
{
/// <summary>
/// The trigger properties.
/// </summary>
[LocalizedRequired]
public RuleTriggerDto Trigger { get; set; }
/// <summary>
/// The action properties.
/// </summary>
[LocalizedRequired]
public Dictionary<string, object> Action { get; set; }
}

81
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRuleDto.cs

@ -0,0 +1,81 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NodaTime;
using Squidex.Infrastructure;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models;
public sealed class DynamicRuleDto : Resource
{
/// <summary>
/// The ID of the rule.
/// </summary>
public DomainId Id { get; set; }
/// <summary>
/// The user that has created the rule.
/// </summary>
public RefToken CreatedBy { get; set; }
/// <summary>
/// The user that has updated the rule.
/// </summary>
public RefToken LastModifiedBy { get; set; }
/// <summary>
/// The date and time when the rule has been created.
/// </summary>
public Instant Created { get; set; }
/// <summary>
/// The date and time when the rule has been modified last.
/// </summary>
public Instant LastModified { get; set; }
/// <summary>
/// The version of the rule.
/// </summary>
public long Version { get; set; }
/// <summary>
/// Determines if the rule is enabled.
/// </summary>
public bool IsEnabled { get; set; }
/// <summary>
/// Optional rule name.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The trigger properties.
/// </summary>
public RuleTriggerDto Trigger { get; set; }
/// <summary>
/// The action properties.
/// </summary>
public Dictionary<string, object> Action { get; set; }
/// <summary>
/// The number of completed executions.
/// </summary>
public long NumSucceeded { get; set; }
/// <summary>
/// The number of failed executions.
/// </summary>
public long NumFailed { get; set; }
/// <summary>
/// The date and time when the rule was executed the last time.
/// </summary>
[Obsolete("Removed when migrated to new rule statistics.")]
public Instant? LastExecuted { get; set; }
}

24
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicRulesDto.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models;
public sealed class DynamicRulesDto : Resource
{
/// <summary>
/// The rules.
/// </summary>
public DynamicRuleDto[] Items { get; set; }
/// <summary>
/// The ID of the rule that is currently rerunning.
/// </summary>
public DomainId? RunningRuleId { get; set; }
}

34
backend/src/Squidex/Areas/Api/Controllers/Rules/Models/DynamicUpdateRuleDto.cs

@ -0,0 +1,34 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models;
[OpenApiRequest]
public sealed class DynamicUpdateRuleDto
{
/// <summary>
/// Optional rule name.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The trigger properties.
/// </summary>
public RuleTriggerDto? Trigger { get; set; }
/// <summary>
/// The action properties.
/// </summary>
public Dictionary<string, object>? Action { get; set; }
/// <summary>
/// Enable or disable the rule.
/// </summary>
public bool? IsEnabled { get; set; }
}

6
backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDtoDto.cs → backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure.UsageTracking;
namespace Squidex.Areas.Api.Controllers.Statistics.Models;
public sealed class CallsUsageDtoDto
public sealed class CallsUsageDto
{
/// <summary>
/// The total number of API calls.
@ -57,9 +57,9 @@ public sealed class CallsUsageDtoDto
/// </summary>
public Dictionary<string, CallsUsagePerDateDto[]> Details { get; set; }
public static CallsUsageDtoDto FromDomain(Plan plan, ApiStatsSummary summary, Dictionary<string, List<ApiStats>> details)
public static CallsUsageDto FromDomain(Plan plan, ApiStatsSummary summary, Dictionary<string, List<ApiStats>> details)
{
return new CallsUsageDtoDto
return new CallsUsageDto
{
AverageElapsedMs = summary.AverageElapsedMs,
BlockingApiCalls = plan.BlockingApiCalls,

8
backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs

@ -93,7 +93,7 @@ public sealed class UsagesController(
/// <response code="404">App not found.</response>
[HttpGet]
[Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")]
[ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(CallsUsageDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppUsage)]
[ApiCosts(0)]
public async Task<IActionResult> GetUsages(string app, DateOnly fromDate, DateOnly toDate)
@ -103,7 +103,7 @@ public sealed class UsagesController(
// Use the current app plan to show the limits to the user.
var (plan, _, _) = await usageGate.GetPlanForAppAsync(App, false, HttpContext.RequestAborted);
var response = CallsUsageDtoDto.FromDomain(plan, summary, details);
var response = CallsUsageDto.FromDomain(plan, summary, details);
return Ok(response);
}
@ -118,7 +118,7 @@ public sealed class UsagesController(
/// <response code="404">Team not found.</response>
[HttpGet]
[Route("teams/{team}/usages/calls/{fromDate}/{toDate}/")]
[ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(CallsUsageDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.TeamUsage)]
[ApiCosts(0)]
public async Task<IActionResult> GetUsagesForTeam(string team, DateOnly fromDate, DateOnly toDate)
@ -128,7 +128,7 @@ public sealed class UsagesController(
// Use the current team plan to show the limits to the user.
var (plan, _) = await usageGate.GetPlanForTeamAsync(Team, HttpContext.RequestAborted);
var response = CallsUsageDtoDto.FromDomain(plan, summary, details);
var response = CallsUsageDto.FromDomain(plan, summary, details);
return Ok(response);
}

1
backend/src/Squidex/Startup.cs

@ -71,7 +71,6 @@ public sealed class Startup(IConfiguration config)
public void Configure(IApplicationBuilder app)
{
app.UseWebSockets();
app.UseCookiePolicy();
app.UseDefaultPathBase();

22
frontend/generator/Generator.sln

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35707.178 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generator", "Generator\Generator.csproj", "{0BD7AC0B-640B-4D26-A9B9-C1CCEE4C14E7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0BD7AC0B-640B-4D26-A9B9-C1CCEE4C14E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0BD7AC0B-640B-4D26-A9B9-C1CCEE4C14E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0BD7AC0B-640B-4D26-A9B9-C1CCEE4C14E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0BD7AC0B-640B-4D26-A9B9-C1CCEE4C14E7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

30
frontend/generator/Generator/Generator.csproj

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NJsonSchema.CodeGeneration.TypeScript" Version="11.2.0" />
<PackageReference Include="NSwag.CodeGeneration.TypeScript" Version="14.3.0" />
</ItemGroup>
<ItemGroup>
<None Update="Templates\ConvertToClass.liquid">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Templates\Class.liquid">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Templates\ConvertToJavaScript.liquid">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Templates\Enum.StringLiteral.liquid">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

165
frontend/generator/Generator/Program.cs

@ -0,0 +1,165 @@
using NJsonSchema;
using NJsonSchema.CodeGeneration.TypeScript;
using NSwag;
using NSwag.CodeGeneration.TypeScript;
using System.Text.RegularExpressions;
namespace Generator;
internal partial class Program
{
static async Task Main(string[] args)
{
var cacheFile = "cache.json";
if (!File.Exists(cacheFile) || !args.Contains("--cache"))
{
var httpClient = new HttpClient();
var schemaResponse = await httpClient.GetAsync("https://localhost:5001/api/swagger/v1/swagger.json");
var schemaText = await schemaResponse.Content.ReadAsStringAsync();
File.WriteAllText(cacheFile, schemaText);
}
var codePath = GetCodePath();
var document = await OpenApiDocument.FromJsonAsync(File.ReadAllText(cacheFile));
foreach (var (typeName, schema) in document.Components.Schemas.ToList())
{
if (typeName.Equals("AssetDto"))
{
if (schema.ActualProperties.TryGetValue("tags", out var tags))
{
tags.IsNullableRaw = false;
tags.IsRequired = true;
}
}
if (typeName.Equals("LanguageDto"))
{
schema.Properties.Remove("nativeName");
}
if (typeName.Equals("AppDto") || typeName.Equals("TeamDto"))
{
if (schema.ActualProperties.ContainsKey("created") && !schema.ActualProperties.ContainsKey("createdBy"))
{
schema.Properties["createdBy"] = new JsonSchemaProperty
{
Description = "The user that has created the app.",
Type = JsonObjectType.String,
IsNullableRaw = true,
IsRequired = false,
IsReadOnly = true,
};
}
if (schema.ActualProperties.ContainsKey("lastModified") && !schema.ActualProperties.ContainsKey("lastModifiedBy"))
{
schema.Properties["lastModifiedBy"] = new JsonSchemaProperty
{
Description = "The user that has updated the app.",
Type = JsonObjectType.String,
IsNullableRaw = true,
IsRequired = false,
IsReadOnly = true,
};
}
}
foreach (var (name, property) in schema.ActualProperties)
{
property.IsReadOnly = true;
if (property.Type == JsonObjectType.String && !property.IsRequired)
{
property.IsNullableRaw = true;
}
if (name.Equals("referenceFields"))
{
property.IsNullableRaw = false;
property.IsRequired = true;
}
if (name.Equals("schemaName"))
{
property.IsNullableRaw = false;
property.IsRequired = true;
}
if (name.Equals("schemaDisplayName"))
{
property.IsNullableRaw = false;
property.IsRequired = true;
}
}
if (document.Components.Schemas.TryGetValue("ErrorDto", out var error))
{
document.Components.Schemas.Remove("ErrorDto");
document.Components.Schemas["ServerErrorDto"] = error;
}
if (!typeName.EndsWith("Dto") && schema.Type == JsonObjectType.Object)
{
document.Components.Schemas.Remove(typeName);
document.Components.Schemas[$"{typeName}Dto"] = schema;
}
}
var extensionFile = Path.Combine(codePath, @"..\\..\\src\\app\\shared\\model\\custom.ts");
var extensionCode = File.ReadAllText(extensionFile);
var classes =
ClassNameRegex().Matches(extensionCode)
.Select(m => m.Groups["ClassName"].Value)
.ToArray();
var settings = new TypeScriptClientGeneratorSettings
{
GenerateClientClasses = false,
GenerateClientInterfaces = false,
};
settings.TypeScriptGeneratorSettings.EnumStyle = TypeScriptEnumStyle.StringLiteral;
settings.TypeScriptGeneratorSettings.ExportTypes = true;
settings.TypeScriptGeneratorSettings.ExtendedClasses = classes;
settings.TypeScriptGeneratorSettings.ExtensionCode = extensionCode;
settings.TypeScriptGeneratorSettings.GenerateConstructorInterface = true;
settings.TypeScriptGeneratorSettings.InlineNamedDictionaries = true;
settings.TypeScriptGeneratorSettings.TemplateDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Templates");
var generator = new TypeScriptClientGenerator(document, settings);
var code = generator.GenerateFile();
code = code.Replace("I{ [key: string]", "{ [key: string]");
code = code.Replace(": Date,", ": DateTime,");
code = code.Replace(": Date;", ": DateTime;");
code = code.Replace(": Date |", ": DateTime |");
code = code.Replace("DtoDto", "Dto");
var targetFolder = Path.Combine(codePath, @"..\\..\\src\\app\\shared\\model\\generated.ts");
File.WriteAllText(targetFolder, code);
}
[GeneratedRegex("class (?<ClassName>[^\\)]*) extends generated\\.")]
private static partial Regex ClassNameRegex();
private static string GetCodePath()
{
var folder = new DirectoryInfo(Directory.GetCurrentDirectory());
while (true)
{
if (folder.Name.Equals("Generator"))
{
return folder.FullName;
}
folder = folder.Parent!;
}
}
}

205
frontend/generator/Generator/Templates/Class.liquid

@ -0,0 +1,205 @@
{% if HasDescription -%}
/** {{ Description }} */
{% endif -%}
{% if ExportTypes %}export {% endif %}{% if IsAbstract %}abstract {% endif %}class {{ ClassName }}{{ Inheritance }} {
{% if HasDiscriminator -%}
/** The discriminator. */
public readonly {{ BaseDiscriminator }}!: string;
{% endif -%}
{% unless HasInheritance -%}
/** Uses the cache values because the actual object is frozen. */
private readonly cachedValues: { [key: string]: any } = {};
{% endunless -%}
{% for property in Properties -%}
{% if property.HasDescription -%}
/** {{ property.Description | strip }} */
{% endif -%}
{% if property.IsReadOnly %}readonly {% endif %}{{ property.PropertyName }}{% if property.IsOptional %}?{% elsif RequiresStrictPropertyInitialization %}!{% endif %}: {{ property.Type }}{{ property.TypePostfix }};
{% endfor -%}
{% if HasIndexerProperty -%}
[key: string]: {{ IndexerPropertyValueType }};
{% endif -%}
{% assign condition_temp = HasInheritance == false or ConvertConstructorInterfaceData -%}
{% if GenerateConstructorInterface or HasBaseDiscriminator -%}
constructor({% if GenerateConstructorInterface %}data?: I{{ ClassName }}{% endif %}) {
{% if HasInheritance -%}
super({% if GenerateConstructorInterface %}data{% endif %});
{% endif -%}
{% if GenerateConstructorInterface and condition_temp -%}
if (data) {
{% if HasInheritance == false -%}
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
{% endif -%}
{% if ConvertConstructorInterfaceData -%}
{% for property in Properties -%}
{% if property.SupportsConstructorConversion -%}
{% if property.IsArray -%}
if (data.{{ property.PropertyName }}) {
this.{{ property.PropertyName }} = [];
for (let i = 0; i < data.{{ property.PropertyName }}.length; i++) {
let item = data.{{ property.PropertyName }}[i];
this.{{ property.PropertyName }}[i] = item && !(<any>item).toJSON ? new {{ property.ArrayItemType }}(item) : <{{ property.ArrayItemType }}>item;
}
}
{% elsif property.IsDictionary -%}
if (data.{{ property.PropertyName }}) {
this.{{ property.PropertyName }} = {};
for (let key in data.{{ property.PropertyName }}) {
if (data.{{ property.PropertyName }}.hasOwnProperty(key)) {
let item = data.{{ property.PropertyName }}[key];
this.{{ property.PropertyName }}[key] = item && !(<any>item).toJSON ? new {{ property.DictionaryItemType }}(item) : <{{ property.DictionaryItemType }}>item;
}
}
}
{% else -%}
this.{{ property.PropertyName }} = data.{{ property.PropertyName }} && !(<any>data.{{ property.PropertyName }}).toJSON ? new {{ property.Type }}(data.{{ property.PropertyName }}) : <{{ property.Type }}>this.{{ property.PropertyName }};
{% endif -%}
{% endif -%}
{% endfor -%}
{% endif -%}
}
{% endif -%}
{% if HasBaseDiscriminator -%}
(<any>this).{{ BaseDiscriminator }} = "{{ DiscriminatorName }}";
{% endif -%}
}
{% endif -%}
{% if HasInheritance and SupportsOverrideKeyword %}override {% endif %}init(_data: any{% if HandleReferences %}, _mappings?: any{% endif %}) {
{% if HasInheritance -%}
super.init(_data);
{% endif -%}
{% if HasIndexerProperty or HasProperties -%}
{% if HasIndexerProperty -%}
for (var property in _data) {
if (_data.hasOwnProperty(property))
this[property] = _data[property];
}
{% endif -%}
{% for property in Properties -%}
{{ property.ConvertToClassCode | strip | tab }}
{% endfor -%}
{% endif -%}
this.cleanup(this);
return this;
}
static {% if HasInheritance and SupportsOverrideKeyword %}override {% endif %}fromJSON(data: any{% if HandleReferences %}, _mappings?: any{% endif %}): {{ ClassName }} {
{% if HandleReferences -%}
{% if HasBaseDiscriminator -%}
{% for derivedClass in DerivedClasses -%}
if (data["{{ BaseDiscriminator }}"] === "{{ derivedClass.Discriminator }}")
{% if derivedClass.IsAbstract -%}
throw new Error("The abstract class '{{ derivedClass.ClassName }}' cannot be instantiated.");
{% else -%}
return createInstance<{{ derivedClass.ClassName }}>(data, _mappings, {{ derivedClass.ClassName }});
{% endif -%}
{% endfor -%}
{% endif -%}
{% if IsAbstract -%}
throw new Error("The abstract class '{{ ClassName }}' cannot be instantiated.");
{% else -%}
return createInstance<{{ ClassName }}>(data, _mappings, {{ ClassName }});
{% endif -%}
{% else -%}
{% if HasBaseDiscriminator -%}
{% for derivedClass in DerivedClasses -%}
if (data["{{ BaseDiscriminator }}"] === "{{ derivedClass.Discriminator }}") {
{% if derivedClass.IsAbstract -%}
throw new Error("The abstract class '{{ derivedClass.ClassName }}' cannot be instantiated.");
{% else -%}
return new {{ derivedClass.ClassName }}().init(data);
{% endif -%}
}
{% endfor -%}
{% endif -%}
{% if IsAbstract -%}
throw new Error("The abstract class '{{ ClassName }}' cannot be instantiated.");
{% else -%}
const result = new {{ ClassName }}().init(data);
result.cleanup(this);
return result;
{% endif -%}
{% endif -%}
}
{% if HasInheritance and SupportsOverrideKeyword %}override {% endif %}toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
{% if HasIndexerProperty -%}
for (var property in this) {
if (this.hasOwnProperty(property))
data[property] = this[property];
}
{% endif -%}
{% if HasDiscriminator -%}
data["{{ BaseDiscriminator }}"] = this.{{ BaseDiscriminator }};
{% endif -%}
{% for property in Properties -%}
{{ property.ConvertToJavaScriptCode | tab }}
{% endfor -%}
{% if HasInheritance -%}
super.toJSON(data);
{% endif -%}
this.cleanup(data);
return data;
}
{% if GenerateCloneMethod -%}
clone(): {{ ClassName }} {
{% if IsAbstract -%}
throw new Error("The abstract class '{{ ClassName }}' cannot be instantiated.");
{% else -%}
const json = this.toJSON();
let result = new {{ ClassName }}();
result.init(json);
return result;
{% endif -%}
}
{% endif -%}
{% unless HasInheritance -%}
protected cleanup(target: any) {
for (var property in target) {
if (target.hasOwnProperty(property)) {
const value = target[property];
if (value === undefined) {
delete target[property];
}
}
}
}
protected compute<T>(key: string, action: () => T): T {
if (!this.cachedValues.hasOwnProperty(key)) {
const value = action();
this.cachedValues[key] = value;
return value;
} else {
return this.cachedValues[key] as any;
}
}
{% endunless -%}
}
{% if GenerateConstructorInterface -%}
{% if HasDescription -%}
/** {{ Description }} */
{% endif -%}
{% if ExportTypes %}export {% endif %}interface I{{ ClassName }}{{ InterfaceInheritance }} {
{% for property in Properties -%}
{% if property.HasDescription -%}
/** {{ property.Description | strip }} */
{% endif -%}
readonly {{ property.PropertyName }}{% if property.IsOptional %}?{% endif %}: {{ property.ConstructorInterfaceType }}{{ property.TypePostfix }};
{% endfor -%}
{% if HasIndexerProperty -%}
[key: string]: {{ IndexerPropertyValueType }};
{% endif -%}
}
{% endif -%}

65
frontend/generator/Generator/Templates/ConvertToClass.liquid

@ -0,0 +1,65 @@
{% if IsNewableObject -%}
{% if CheckNewableObject -%}
{{ Variable }} = {{ Value }} ? {{ Type }}.fromJSON({{ Value }}{% if HandleReferences -%}, _mappings{% endif %}) : {% if HasDefaultValue %}{{ DefaultValue }}{% else %}<any>{{ NullValue }}{% endif %};
{% else -%}
{{ Variable }} = {{ Type }}.fromJSON({{ Value }}{% if HandleReferences -%}, _mappings{% endif %});
{% endif -%}
{% elsif IsArray -%}
if (Array.isArray({{ Value }})) {
{{ Variable }} = [] as any;
for (let item of {{ Value }})
{% if IsArrayItemNewableObject -%}
{{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push({{ ArrayItemType }}.fromJSON(item{% if HandleReferences %}, _mappings{% endif %}));
{% else -%}
{% if IsArrayItemDate -%}
{{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push(DateTime.parseISO(item));
{% elsif IsArrayItemDateTime -%}
{{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push(DateTime.parseISO(item));
{% else -%}
{{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push(item);
{% endif -%}
{% endif -%}
}
{% if NullValue != "undefined" %}else {
{{ Variable }} = <any>{{ NullValue }};
}
{% endif -%}
{% elsif IsDictionary -%}
if ({{ Value }}) {
{{ Variable }} = {} as any;
for (let key in {{ Value }}) {
if ({{ Value }}.hasOwnProperty(key))
{% if IsDictionaryValueNewableObject -%}
(<any>{{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? {{ DictionaryValueType }}.fromJSON({{ Value }}[key]{% if HandleReferences %}, _mappings{% endif %}) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}<any>{{ NullValue }}{% endif %};
{% elsif IsDictionaryValueNewableArray -%}
(<any>{{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? {{ Value }}[key].map((i: any) => {{ DictionaryValueArrayItemType }}.fromJSON(i{% if HandleReferences %}, _mappings{% endif %})) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}<any>{{ NullValue }}{% endif %};
{% elsif IsDictionaryValueDate -%}
(<any>{{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? DateTime.parseISO({{ Value }}[key].toString()) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}<any>{{ NullValue }}{% endif %};
{% elsif IsDictionaryValueDateTime -%}
(<any>{{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? DateTime.parseISO({{ Value }}[key].toString()) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}<any>{{ NullValue }}{% endif %};
{% else -%}
{% if HasDictionaryValueDefaultValue or NullValue != "undefined" -%}
(<any>{{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] !== undefined ? {{ Value }}[key] : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}<any>{{ NullValue }}{% endif %};
{% else -%}
(<any>{{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key];
{% endif -%}
{% endif -%}
}
}
{% if NullValue != "undefined" %}else {
{{ Variable }} = <any>{{ NullValue }};
}
{% endif -%}
{% else -%}
{% if IsDate -%}
{{ Variable }} = {{ Value }} ? DateTime.parseISO({{ Value }}.toString()) : {% if HasDefaultValue %}DateTime.parseISO({{ DefaultValue }}){% else %}<any>{{ NullValue }}{% endif %};
{% elsif IsDateTime -%}
{{ Variable }} = {{ Value }} ? DateTime.parseISO({{ Value }}.toString()) : {% if HasDefaultValue %}DateTime.parseISO({{ DefaultValue }}){% else %}<any>{{ NullValue }}{% endif %};
{% else -%}
{% if HasDefaultValue or NullValue != "undefined" -%}
{{ Variable }} = {{ Value }} !== undefined ? {{ Value }} : {% if HasDefaultValue %}{{ DefaultValue }}{% else %}<any>{{ NullValue }}{% endif %};
{% else -%}
{{ Variable }} = {{ Value }};
{% endif -%}
{% endif -%}
{% endif -%}

45
frontend/generator/Generator/Templates/ConvertToJavaScript.liquid

@ -0,0 +1,45 @@
{%- if IsNewableObject -%}
{{ Variable }} = {{ Value }} ? {{ Value }}.toJSON() : <any>{{ NullValue }};
{%- elsif IsArray -%}
if (Array.isArray({{ Value }})) {
{{ Variable }} = [];
for (let item of {{ Value }})
{%- if IsArrayItemNewableObject -%}
{{ Variable }}.push(item.toJSON());
{%- elsif IsArrayItemDate -%}
{{ Variable }}.push({% if UseJsDate %}formatDate(item){% else %}item.toISOString(){% endif %});
{%- elsif IsArrayItemDateTime -%}
{{ Variable }}.push(item.toISOString());
{%- else -%}
{{ Variable }}.push(item);
{%- endif -%}
}
{%- elsif IsDictionary -%}
if ({{ Value }}) {
{{ Variable }} = {};
for (let key in {{ Value }}) {
if ({{ Value }}.hasOwnProperty(key))
{%- if IsDictionaryValueNewableObject -%}
(<any>{{ Variable }})[key] = {{ Value }}[key] ? {{ Value }}[key].toJSON() : <any>{{ NullValue }};
{%- elsif IsDictionaryValueDate -%}
(<any>{{ Variable }})[key] = {{ Value }}[key] ? {{ Value }}[key].toISOString() : <any>{{ NullValue }};
{%- elsif IsDictionaryValueDateTime -%}
(<any>{{ Variable }})[key] = {{ Value }}[key] ? {{ Value }}[key].toISOString() : <any>{{ NullValue }};
{%- else -%}
{%- if NullValue != "undefined" -%}
(<any>{{ Variable }})[key] = {{ Value }}[key] !== undefined ? {{ Value }}[key] : <any>{{ NullValue }};
{%- else -%}
(<any>{{ Variable }})[key] = (<any>{{ Value }})[key];
{%- endif -%}
{%- endif -%}
}
}
{%- elsif IsDate -%}
{{ Variable }} = {{ Value }} ? {{ Value }}.toISOString() : {% if HasDefaultValue %}{{ DefaultValue }}{% else %}<any>{{ NullValue }}{% endif %};
{%- elsif IsDateTime -%}
{{ Variable }} = {{ Value }} ? {{ Value }}.toISOString() : {% if HasDefaultValue %}{{ DefaultValue }}{% else %}<any>{{ NullValue }}{% endif %};
{%- elsif NullValue != "undefined" -%}
{{ Variable }} = {{ Value }} !== undefined ? {{ Value }} : <any>{{ NullValue }};
{%- else -%}
{{ Variable }} = {{ Value }};
{%- endif %}

10
frontend/generator/Generator/Templates/Enum.StringLiteral.liquid

@ -0,0 +1,10 @@
{%- if HasDescription -%}
/** {{ Description }} */
{%- endif -%}
{%- if ExportTypes %}export {% endif %}type {{ Name }} = {% for enumeration in Enums %}{%- if Enums.first.Value != enumeration.Value %} | {% endif %}{{ enumeration.Value }}{% endfor %};
export const {{ Name | uppercamelcase }}Values: ReadonlyArray<{{ Name }}> = [
{% for enumeration in Enums -%}
{{ enumeration.Value }}{% unless forloop.last %},{% endunless %}
{% endfor -%}
];

4
frontend/src/app/features/administration/pages/event-consumers/event-consumer.component.ts

@ -9,8 +9,8 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { TooltipDirective } from '@app/shared';
import { EventConsumerDto, EventConsumersState } from '../../internal';
import { EventConsumerDto, TooltipDirective } from '@app/shared';
import { EventConsumersState } from '../../internal';
@Component({
standalone: true,

4
frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts

@ -10,8 +10,8 @@ import { Component, OnInit } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { timer } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { DialogModel, LayoutComponent, ListViewComponent, ModalDialogComponent, ModalDirective, ShortcutDirective, SidebarMenuDirective, Subscriptions, SyncWidthDirective, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared';
import { EventConsumerDto, EventConsumersState } from '../../internal';
import { DialogModel, EventConsumerDto, LayoutComponent, ListViewComponent, ModalDialogComponent, ModalDirective, ShortcutDirective, SidebarMenuDirective, Subscriptions, SyncWidthDirective, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared';
import { EventConsumersState } from '../../internal';
import { EventConsumerComponent } from './event-consumer.component';
@Component({

25
frontend/src/app/features/administration/pages/restore/restore-page.component.ts

@ -50,19 +50,20 @@ export class RestorePageComponent {
public restore() {
const value = this.restoreForm.submit();
if (!value) {
return;
}
if (value) {
this.restoreForm.submitCompleted();
this.restoreForm.submitCompleted();
this.jobsService.postRestore(value)
.subscribe({
next: () => {
this.dialogs.notifyInfo('i18n:jobs.restoreStarted');
},
error: error => {
this.dialogs.notifyError(error);
},
});
}
this.jobsService.postRestore(value)
.subscribe({
next: () => {
this.dialogs.notifyInfo('i18n:jobs.restoreStarted');
},
error: error => {
this.dialogs.notifyError(error);
},
});
}
}

51
frontend/src/app/features/administration/pages/users/user-page.component.ts

@ -9,8 +9,8 @@ import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ControlErrorsComponent, FormErrorComponent, LayoutComponent, ShortcutDirective, Subscriptions, TitleComponent, TooltipDirective, TranslatePipe } from '@app/shared';
import { UpsertUserDto, UserDto, UserForm, UsersState } from '../../internal';
import { ControlErrorsComponent, FormErrorComponent, LayoutComponent, ShortcutDirective, Subscriptions, TitleComponent, TooltipDirective, TranslatePipe, UpdateUserDto } from '@app/shared';
import { UserDto, UserForm, UsersState } from '../../internal';
@Component({
standalone: true,
@ -66,29 +66,32 @@ export class UserPageComponent implements OnInit {
}
const value = this.userForm.submit();
if (!value) {
return;
}
if (this.user) {
const request = new UpdateUserDto({ ...value });
if (value) {
if (this.user) {
this.usersState.update(this.user, value)
.subscribe({
next: user => {
this.userForm.submitCompleted({ newValue: user });
},
error: error => {
this.userForm.submitFailed(error);
},
});
} else {
this.usersState.create(<UpsertUserDto>value)
.subscribe({
next: () => {
this.back();
},
error: error => {
this.userForm.submitFailed(error);
},
});
}
this.usersState.update(this.user, request)
.subscribe({
next: user => {
this.userForm.submitCompleted({ newValue: user });
},
error: error => {
this.userForm.submitFailed(error);
},
});
} else {
this.usersState.create(value)
.subscribe({
next: () => {
this.back();
},
error: error => {
this.userForm.submitFailed(error);
},
});
}
}

60
frontend/src/app/features/administration/services/event-consumers.service.spec.ts

@ -8,8 +8,9 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, Resource, ResourceLinks } from '@app/shared';
import { EventConsumerDto, EventConsumersDto, EventConsumersService } from './event-consumers.service';
import { ApiUrlConfig, EventConsumerDto, EventConsumersDto } from '@app/shared';
import { IResourceDto, ResourceLinkDto } from '@app/shared/model';
import { EventConsumersService } from './event-consumers.service';
describe('EventConsumersService', () => {
beforeEach(() => {
@ -31,7 +32,6 @@ describe('EventConsumersService', () => {
it('should make get request to get event consumers',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
let eventConsumers: EventConsumersDto;
eventConsumersService.getEventConsumers().subscribe(result => {
eventConsumers = result;
});
@ -46,26 +46,28 @@ describe('EventConsumersService', () => {
eventConsumerResponse(12),
eventConsumerResponse(13),
],
_links: {},
});
expect(eventConsumers!).toEqual({
items: [
createEventConsumer(12),
createEventConsumer(13),
],
});
expect(eventConsumers!).toEqual(
new EventConsumersDto({
items: [
createEventConsumer(12),
createEventConsumer(13),
],
_links: {},
}));
}));
it('should make put request to start event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
const resource: Resource = {
const resource: IResourceDto = {
_links: {
start: { method: 'PUT', href: 'api/event-consumers/event-consumer123/start' },
start: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/start' }),
},
};
let eventConsumer: EventConsumerDto;
eventConsumersService.putStart(resource).subscribe(response => {
eventConsumer = response;
});
@ -82,14 +84,13 @@ describe('EventConsumersService', () => {
it('should make put request to stop event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
const resource: Resource = {
const resource: IResourceDto = {
_links: {
stop: { method: 'PUT', href: 'api/event-consumers/event-consumer123/stop' },
stop: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/stop' }),
},
};
let eventConsumer: EventConsumerDto;
eventConsumersService.putStop(resource).subscribe(response => {
eventConsumer = response;
});
@ -106,14 +107,13 @@ describe('EventConsumersService', () => {
it('should make put request to reset event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
const resource: Resource = {
const resource: IResourceDto = {
_links: {
reset: { method: 'PUT', href: 'api/event-consumers/event-consumer123/reset' },
reset: new ResourceLinkDto({ method: 'PUT', href: 'api/event-consumers/event-consumer123/reset' }),
},
};
let eventConsumer: EventConsumerDto;
eventConsumersService.putReset(resource).subscribe(response => {
eventConsumer = response;
});
@ -146,17 +146,17 @@ describe('EventConsumersService', () => {
});
export function createEventConsumer(id: number, suffix = '') {
const links: ResourceLinks = {
reset: { method: 'PUT', href: `/event-consumers/${id}/reset` },
};
const key = `${id}${suffix}`;
return new EventConsumerDto(links,
`event-consumer${id}`,
id,
true,
true,
`failure${key}`,
`position${key}`);
}
return new EventConsumerDto({
name: `event-consumer${id}`,
position: `position${key}`,
count: id,
isStopped: true,
isResetting: true,
error: `failure${key}`,
_links: {
reset: new ResourceLinkDto({ method: 'PUT', href: `/event-consumers/${id}/reset` }),
},
});
}

61
frontend/src/app/features/administration/services/event-consumers.service.ts

@ -9,35 +9,7 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiUrlConfig, hasAnyLink, pretifyError, Resource, ResourceLinks } from '@app/shared';
export class EventConsumerDto implements Resource {
public readonly _links: ResourceLinks;
public readonly canReset: boolean;
public readonly canStart: boolean;
public readonly canStop: boolean;
constructor(links: ResourceLinks,
public readonly name: string,
public readonly count: number,
public readonly isStopped?: boolean,
public readonly isResetting?: boolean,
public readonly error?: string,
public readonly position?: string,
) {
this._links = links;
this.canReset = hasAnyLink(links, 'reset');
this.canStart = hasAnyLink(links, 'start');
this.canStop = hasAnyLink(links, 'stop');
}
}
export type EventConsumersDto = Readonly<{
// The list of event consumers.
items: ReadonlyArray<EventConsumerDto>;
}>;
import { ApiUrlConfig, EventConsumerDto, EventConsumersDto, IResourceDto, pretifyError } from '@app/shared';
@Injectable()
export class EventConsumersService {
@ -52,61 +24,44 @@ export class EventConsumersService {
return this.http.get<any>(url).pipe(
map(body => {
return parseEventConsumers(body);
return EventConsumersDto.fromJSON(body);
}),
pretifyError('i18n:eventConsumers.loadFailed'));
}
public putStart(eventConsumer: Resource): Observable<EventConsumerDto> {
public putStart(eventConsumer: IResourceDto): Observable<EventConsumerDto> {
const link = eventConsumer._links['start'];
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
map(body => {
return parseEventConsumer(body);
return EventConsumerDto.fromJSON(body);
}),
pretifyError('i18n:eventConsumers.startFailed'));
}
public putStop(eventConsumer: Resource): Observable<EventConsumerDto> {
public putStop(eventConsumer: IResourceDto): Observable<EventConsumerDto> {
const link = eventConsumer._links['stop'];
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
map(body => {
return parseEventConsumer(body);
return EventConsumerDto.fromJSON(body);
}),
pretifyError('i18n:eventConsumers.stopFailed'));
}
public putReset(eventConsumer: Resource): Observable<EventConsumerDto> {
public putReset(eventConsumer: IResourceDto): Observable<EventConsumerDto> {
const link = eventConsumer._links['reset'];
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
map(body => {
return parseEventConsumer(body);
return EventConsumerDto.fromJSON(body);
}),
pretifyError('i18n:eventConsumers.resetFailed'));
}
}
function parseEventConsumers(response: { items: any[] } & Resource): EventConsumersDto {
const { items: list } = response;
const items = list.map(parseEventConsumer);
return { items };
}
function parseEventConsumer(response: any): EventConsumerDto {
return new EventConsumerDto(response._links,
response.name,
response.count,
response.isStopped,
response.isResetting,
response.error,
response.position);
}

100
frontend/src/app/features/administration/services/users.service.spec.ts

@ -8,8 +8,9 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, Resource, ResourceLinks } from '@app/shared';
import { UserDto, UsersDto, UsersService } from './users.service';
import { ApiUrlConfig, UserDto, UsersDto } from '@app/shared';
import { CreateUserDto, IResourceDto, ResourceLinkDto, UpdateUserDto } from '@app/shared/model';
import { UsersService } from './users.service';
describe('UsersService', () => {
beforeEach(() => {
@ -31,7 +32,6 @@ describe('UsersService', () => {
it('should make get request to get many users',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto;
userManagementService.getUsers(20, 30).subscribe(result => {
users = result;
});
@ -47,22 +47,23 @@ describe('UsersService', () => {
userResponse(12),
userResponse(13),
],
_links: {},
});
expect(users!).toEqual({
total: 100,
items: [
createUser(12),
createUser(13),
],
canCreate: false,
});
expect(users!).toEqual(
new UsersDto({
total: 100,
items: [
createUser(12),
createUser(13),
],
_links: {},
}));
}));
it('should make get request with query to get many users',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto;
userManagementService.getUsers(20, 30, 'my-query').subscribe(result => {
users = result;
});
@ -78,22 +79,23 @@ describe('UsersService', () => {
userResponse(12),
userResponse(13),
],
_links: {},
});
expect(users!).toEqual({
total: 100,
items: [
createUser(12),
createUser(13),
],
canCreate: false,
});
expect(users!).toEqual(
new UsersDto({
total: 100,
items: [
createUser(12),
createUser(13),
],
_links: {},
}));
}));
it('should make get request to get single user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let user: UserDto;
userManagementService.getUser('123').subscribe(result => {
user = result;
});
@ -110,10 +112,14 @@ describe('UsersService', () => {
it('should make post request to create user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = { email: 'mail@squidex.io', displayName: 'Squidex User', permissions: ['Permission1'], password: 'password' };
const dto = new CreateUserDto({
email: 'mail@squidex.io',
displayName: 'Squidex User',
permissions: ['Permission1'],
password: 'password',
});
let user: UserDto;
userManagementService.postUser(dto).subscribe(result => {
user = result;
});
@ -130,16 +136,20 @@ describe('UsersService', () => {
it('should make put request to update user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = { email: 'mail@squidex.io', displayName: 'Squidex User', permissions: ['Permission1'], password: 'password' };
const dto = new UpdateUserDto({
email: 'mail@squidex.io',
displayName: 'Squidex User',
permissions: ['Permission1'],
password: 'password',
});
const resource: Resource = {
const resource: IResourceDto = {
_links: {
update: { method: 'PUT', href: 'api/user-management/123' },
update: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123' }),
},
};
let user: UserDto;
userManagementService.putUser(resource, dto).subscribe(result => {
user = result;
});
@ -156,14 +166,13 @@ describe('UsersService', () => {
it('should make put request to lock user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const resource: Resource = {
const resource: IResourceDto = {
_links: {
lock: { method: 'PUT', href: 'api/user-management/123/lock' },
lock: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123/lock' }),
},
};
let user: UserDto;
userManagementService.lockUser(resource).subscribe(result => {
user = result;
});
@ -180,14 +189,13 @@ describe('UsersService', () => {
it('should make put request to unlock user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const resource: Resource = {
const resource: IResourceDto = {
_links: {
unlock: { method: 'PUT', href: 'api/user-management/123/unlock' },
unlock: new ResourceLinkDto({ method: 'PUT', href: 'api/user-management/123/unlock' }),
},
};
let user: UserDto;
userManagementService.unlockUser(resource).subscribe(result => {
user = result;
});
@ -204,9 +212,9 @@ describe('UsersService', () => {
it('should make delete request to delete user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const resource: Resource = {
const resource: IResourceDto = {
_links: {
delete: { method: 'DELETE', href: 'api/user-management/123' },
delete: new ResourceLinkDto({ method: 'DELETE', href: 'api/user-management/123' }),
},
};
@ -241,18 +249,18 @@ describe('UsersService', () => {
});
export function createUser(id: number, suffix = '') {
const links: ResourceLinks = {
update: { method: 'PUT', href: `/users/${id}` },
};
const key = `${id}${suffix}`;
return new UserDto(links,
`${id}`,
`user${key}@domain.com`,
`user${key}`,
[
return new UserDto({
id: `${id}`,
email: `user${key}@domain.com`,
displayName: `user${key}`,
permissions: [
`Permission${key}`,
],
true);
}
isLocked: true,
_links: {
update: new ResourceLinkDto({ method: 'PUT', href: `/users/${id}` }),
},
});
}

99
frontend/src/app/features/administration/services/users.service.ts

@ -9,56 +9,8 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiUrlConfig, hasAnyLink, pretifyError, Resource, ResourceLinks, StringHelper } from '@app/shared';
export class UserDto implements Resource {
public readonly _links: ResourceLinks;
public readonly canDelete: boolean;
public readonly canLock: boolean;
public readonly canUnlock: boolean;
public readonly canUpdate: boolean;
constructor(links: ResourceLinks,
public readonly id: string,
public readonly email: string,
public readonly displayName: string,
public readonly permissions: ReadonlyArray<string> = [],
public readonly isLocked?: boolean,
) {
this._links = links;
this.canDelete = hasAnyLink(links, 'delete');
this.canLock = hasAnyLink(links, 'lock');
this.canUnlock = hasAnyLink(links, 'unlock');
this.canUpdate = hasAnyLink(links, 'update');
}
}
export type UsersDto = Readonly<{
// The list of users.
items: ReadonlyArray<UserDto>;
// The number of users.
total: number;
// True, if the user has permissions to create a user.
canCreate?: boolean;
}>;
export type UpsertUserDto = Readonly<{
// The email address of the user.
email: string;
// The display name.
displayName?: string;
// The permissions as in the dot-notation.
permissions?: ReadonlyArray<string>;
// The password (confirm is only used in the UI).
password?: string;
}>;
import { ApiUrlConfig, CreateUserDto, IResourceDto, pretifyError, StringHelper, UpdateUserDto, UserDto, UsersDto } from '@app/shared';
export { UserDto, UsersDto };
@Injectable()
export class UsersService {
@ -73,7 +25,7 @@ export class UsersService {
return this.http.get<any>(url).pipe(
map(body => {
return parseUsers(body);
return UsersDto.fromJSON(body);
}),
pretifyError('i18n:users.loadFailed'));
}
@ -83,58 +35,58 @@ export class UsersService {
return this.http.get(url).pipe(
map(body => {
return parseUser(body);
return UserDto.fromJSON(body);
}),
pretifyError('i18n:users.loadUserFailed'));
}
public postUser(dto: UpsertUserDto): Observable<UserDto> {
public postUser(dto: CreateUserDto): Observable<UserDto> {
const url = this.apiUrl.buildUrl('api/user-management');
return this.http.post(url, dto).pipe(
return this.http.post(url, dto.toJSON()).pipe(
map(body => {
return parseUser(body);
return UserDto.fromJSON(body);
}),
pretifyError('i18n:users.createFailed'));
}
public putUser(user: Resource, dto: Partial<UpsertUserDto>): Observable<UserDto> {
public putUser(user: IResourceDto, dto: UpdateUserDto): Observable<UserDto> {
const link = user._links['update'];
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url, { body: dto }).pipe(
return this.http.request(link.method, url, { body: dto.toJSON() }).pipe(
map(body => {
return parseUser(body);
return UserDto.fromJSON(body);
}),
pretifyError('i18n:users.updateFailed'));
}
public lockUser(user: Resource): Observable<UserDto> {
public lockUser(user: IResourceDto): Observable<UserDto> {
const link = user._links['lock'];
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
map(body => {
return parseUser(body);
return UserDto.fromJSON(body);
}),
pretifyError('i18n:users.lockFailed'));
}
public unlockUser(user: Resource): Observable<UserDto> {
public unlockUser(user: IResourceDto): Observable<UserDto> {
const link = user._links['unlock'];
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
map(body => {
return parseUser(body);
return UserDto.fromJSON(body);
}),
pretifyError('i18n:users.unlockFailed'));
}
public deleteUser(user: Resource): Observable<any> {
public deleteUser(user: IResourceDto): Observable<any> {
const link = user._links['delete'];
const url = this.apiUrl.buildUrl(link.href);
@ -142,23 +94,4 @@ export class UsersService {
return this.http.request(link.method, url).pipe(
pretifyError('i18n:users.deleteFailed'));
}
}
function parseUsers(response: { items: any[]; total: number } & Resource): UsersDto {
const { items: list, total, _links } = response;
const items = list.map(parseUser);
const canCreate = hasAnyLink(_links, 'create');
return { items, total, canCreate };
}
function parseUser(response: any) {
return new UserDto(
response._links,
response.id,
response.email,
response.displayName,
response.permissions,
response.isLocked);
}
}

8
frontend/src/app/features/administration/state/event-consumers.state.spec.ts

@ -7,7 +7,7 @@
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService } from '@app/shared';
import { DialogService, EventConsumersDto } from '@app/shared';
import { EventConsumersService } from '../internal';
import { createEventConsumer } from '../services/event-consumers.service.spec';
import { EventConsumersState } from './event-consumers.state';
@ -34,7 +34,7 @@ describe('EventConsumersState', () => {
describe('Loading', () => {
it('should load event consumers', () => {
eventConsumersService.setup(x => x.getEventConsumers())
.returns(() => of({ items: [eventConsumer1, eventConsumer2] })).verifiable();
.returns(() => of(new EventConsumersDto({ items: [eventConsumer1, eventConsumer2], _links: {} }))).verifiable();
eventConsumersState.load().subscribe();
@ -56,7 +56,7 @@ describe('EventConsumersState', () => {
it('should show notification on load if reload is true', () => {
eventConsumersService.setup(x => x.getEventConsumers())
.returns(() => of({ items: [eventConsumer1, eventConsumer2] })).verifiable();
.returns(() => of(new EventConsumersDto({ items: [eventConsumer1, eventConsumer2], _links: {} }))).verifiable();
eventConsumersState.load(true).subscribe();
@ -80,7 +80,7 @@ describe('EventConsumersState', () => {
describe('Updates', () => {
beforeEach(() => {
eventConsumersService.setup(x => x.getEventConsumers())
.returns(() => of({ items: [eventConsumer1, eventConsumer2] })).verifiable();
.returns(() => of(new EventConsumersDto({ items: [eventConsumer1, eventConsumer2], _links: {} }))).verifiable();
eventConsumersState.load().subscribe();
});

4
frontend/src/app/features/administration/state/event-consumers.state.ts

@ -8,8 +8,8 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { finalize, tap } from 'rxjs/operators';
import { debug, DialogService, LoadingState, shareSubscribed, State } from '@app/shared';
import { EventConsumerDto, EventConsumersService } from '../services/event-consumers.service';
import { debug, DialogService, EventConsumerDto, LoadingState, shareSubscribed, State } from '@app/shared';
import { EventConsumersService } from '../services/event-consumers.service';
interface Snapshot extends LoadingState {
// The list of event consumers.

7
frontend/src/app/features/administration/state/users.forms.ts

@ -6,10 +6,9 @@
*/
import { UntypedFormControl, Validators } from '@angular/forms';
import { ExtendedFormGroup, Form, ValidatorsEx } from '@app/shared';
import { UpsertUserDto, UserDto } from '../services/users.service';
import { CreateUserDto, ExtendedFormGroup, Form, UserDto, ValidatorsEx } from '@app/shared';
export class UserForm extends Form<ExtendedFormGroup, UpsertUserDto, UserDto> {
export class UserForm extends Form<ExtendedFormGroup, CreateUserDto, UserDto> {
constructor() {
super(new ExtendedFormGroup({
email: new UntypedFormControl('', [
@ -52,6 +51,6 @@ export class UserForm extends Form<ExtendedFormGroup, UpsertUserDto, UserDto> {
protected transformSubmit(value: any) {
const permissions = value['permissions'].split('\n').defined();
return { ...value, permissions };
return new CreateUserDto({ ...value, permissions });
}
}

32
frontend/src/app/features/administration/state/users.state.spec.ts

@ -7,8 +7,8 @@
import { firstValueFrom, of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService } from '@app/shared';
import { UpsertUserDto, UsersService } from '../internal';
import { CreateUserDto, DialogService, UpdateUserDto, UsersDto } from '@app/shared';
import { UsersService } from '../internal';
import { createUser } from '../services/users.service.spec';
import { UsersState } from './users.state';
@ -36,7 +36,7 @@ describe('UsersState', () => {
describe('Loading', () => {
it('should load users', () => {
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => of({ items: [user1, user2], total: 200 })).verifiable();
.returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable();
usersState.load().subscribe();
@ -59,7 +59,7 @@ describe('UsersState', () => {
it('should show notification on load if reload is true', () => {
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => of({ items: [user1, user2], total: 200 })).verifiable();
.returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable();
usersState.load(true).subscribe();
@ -70,7 +70,7 @@ describe('UsersState', () => {
it('should load with new pagination if paging', () => {
usersService.setup(x => x.getUsers(10, 10, undefined))
.returns(() => of({ items: [], total: 200 })).verifiable();
.returns(() => of(new UsersDto({ items: [], total: 200, _links: {} }))).verifiable();
usersState.page({ page: 1, pageSize: 10 }).subscribe();
@ -79,7 +79,7 @@ describe('UsersState', () => {
it('should load with query if searching', () => {
usersService.setup(x => x.getUsers(10, 0, 'my-query'))
.returns(() => of({ items: [], total: 0 })).verifiable();
.returns(() => of(new UsersDto({ items: [], total: 0, _links: {} }))).verifiable();
usersState.search('my-query').subscribe();
@ -90,7 +90,7 @@ describe('UsersState', () => {
describe('Updates', () => {
beforeEach(() => {
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => of({ items: [user1, user2], total: 200 })).verifiable();
.returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable();
usersState.load().subscribe();
});
@ -130,7 +130,7 @@ describe('UsersState', () => {
});
it('should add user to snapshot if created', () => {
const request: UpsertUserDto = { ...newUser, password: 'password' } as any;
const request = new CreateUserDto({ ...newUser, password: 'password' });
usersService.setup(x => x.postUser(request))
.returns(() => of(newUser)).verifiable();
@ -142,9 +142,8 @@ describe('UsersState', () => {
});
it('should update user if updated', () => {
const request: Partial<UpsertUserDto> = {};
const updated = createUser(2, '_new');
const request = new UpdateUserDto({ ...updated });
usersService.setup(x => x.putUser(user2, request))
.returns(() => of(updated)).verifiable();
@ -187,10 +186,10 @@ describe('UsersState', () => {
});
it('should truncate users if page size reached', () => {
const request: UpsertUserDto = { ...newUser, password: 'password' } as any;
const request = new CreateUserDto({ ...newUser, password: 'password' });
usersService.setup(x => x.getUsers(2, 0, undefined))
.returns(() => of({ items: [user1, user2], total: 200 })).verifiable();
.returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable();
usersService.setup(x => x.postUser(request))
.returns(() => of(newUser)).verifiable();
@ -205,8 +204,8 @@ describe('UsersState', () => {
describe('Selection', () => {
beforeEach(() => {
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => of({ items: [user1, user2], total: 200 })).verifiable(Times.atLeastOnce());
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => of(new UsersDto({ items: [user1, user2], total: 200, _links: {} }))).verifiable(Times.atLeastOnce());
usersState.load().subscribe();
usersState.select(user2.id).subscribe();
@ -219,7 +218,7 @@ describe('UsersState', () => {
];
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => of({ items: newUsers, total: 200 }));
.returns(() => of(new UsersDto({ items: newUsers, total: 200, _links: {} })));
usersState.load().subscribe();
@ -227,9 +226,8 @@ describe('UsersState', () => {
});
it('should update selected user if updated', () => {
const request = {};
const updated = createUser(2, '_new');
const request = new UpdateUserDto({ ...updated });
usersService.setup(x => x.putUser(user2, request))
.returns(() => of(updated)).verifiable();

10
frontend/src/app/features/administration/state/users.state.ts

@ -11,8 +11,8 @@ import { Injectable } from '@angular/core';
import '@app/framework/utils/rxjs-extensions';
import { EMPTY, Observable, of } from 'rxjs';
import { catchError, finalize, tap } from 'rxjs/operators';
import { debug, DialogService, getPagingInfo, ListState, shareSubscribed, State } from '@app/shared';
import { UpsertUserDto, UserDto, UsersService } from '../services/users.service';
import { CreateUserDto, debug, DialogService, getPagingInfo, ListState, shareSubscribed, State, UpdateUserDto, UserDto } from '@app/shared';
import { UsersService } from '../services/users.service';
interface Snapshot extends ListState<string> {
// The current users.
@ -104,7 +104,7 @@ export class UsersState extends State<Snapshot> {
pageSize,
pageSize * page,
query).pipe(
tap(({ total, items: users, canCreate }) => {
tap(({ canCreate, items: users, total }) => {
if (isReload) {
this.dialogs.notifyInfo('i18n:users.reloaded');
}
@ -132,7 +132,7 @@ export class UsersState extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public create(request: UpsertUserDto): Observable<UserDto> {
public create(request: CreateUserDto): Observable<UserDto> {
return this.usersService.postUser(request).pipe(
tap(created => {
this.next(s => {
@ -144,7 +144,7 @@ export class UsersState extends State<Snapshot> {
shareSubscribed(this.dialogs, { silent: true }));
}
public update(user: UserDto, request: Partial<UpsertUserDto>): Observable<UserDto> {
public update(user: UserDto, request: UpdateUserDto): Observable<UserDto> {
return this.usersService.putUser(user, request).pipe(
tap(updated => {
this.replaceUser(updated);

1
frontend/src/app/features/assets/pages/asset-tag-dialog.component.ts

@ -52,7 +52,6 @@ export class AssetTagDialogComponent implements OnInit {
public renameAssetTag() {
const value = this.editForm.submit();
if (!value) {
return;
}

4
frontend/src/app/features/content/pages/calendar/calendar-page.component.ts

@ -9,7 +9,7 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { AppsState, ConfirmClickDirective, ContentDto, ContentsService, ContentStatusComponent, CopyDirective, DateTime, DialogModel, FullDateTimePipe, getContentValue, LanguageDto, LanguagesState, LayoutComponent, LocalizerService, ModalDialogComponent, ModalDirective, ResourceLoaderService, TitleComponent, TooltipDirective, TranslatePipe, UserNameRefPipe, UserPictureRefPipe } from '@app/shared';
import { AppLanguageDto, AppsState, ConfirmClickDirective, ContentDto, ContentsService, ContentStatusComponent, CopyDirective, DateTime, DialogModel, FullDateTimePipe, getContentValue, LanguagesState, LayoutComponent, LocalizerService, ModalDialogComponent, ModalDirective, ResourceLoaderService, TitleComponent, TooltipDirective, TranslatePipe, UserNameRefPipe, UserPictureRefPipe } from '@app/shared';
declare const tui: any;
@ -39,7 +39,7 @@ type ViewMode = 'day' | 'week' | 'month';
})
export class CalendarPageComponent implements AfterViewInit, OnDestroy, OnInit {
private calendar: any;
private language!: LanguageDto;
private language!: AppLanguageDto;
@ViewChild('calendarContainer', { static: false })
public calendarContainer!: ElementRef;

6
frontend/src/app/features/content/pages/content/content-event.component.ts

@ -42,9 +42,9 @@ export class ContentEventComponent {
public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes.event) {
this.canLoadOrCompare =
(this.event.eventType === 'ContentUpdatedEvent' ||
this.event.eventType === 'ContentCreatedEventV2') &&
!this.event.version.eq(this.content.version);
(this.event.eventType === 'ContentUpdatedEvent' ||
this.event.eventType === 'ContentCreatedEventV2') &&
this.event.version !== this.content.version;
}
}
}

5
frontend/src/app/features/content/pages/content/content-page.component.ts

@ -252,7 +252,6 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit {
private saveContent(publish: boolean, navigationMode: SaveNavigationMode) {
const value = this.contentForm.submit();
if (!value) {
this.contentForm.submitFailed('i18n:contents.contentNotValid', false);
return;
@ -365,10 +364,10 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit {
this.loadVersion(null, false);
}
public loadVersion(version: Version | null, compare: boolean) {
public loadVersion(version: number | null, compare: boolean) {
const content = this.content;
if (!content || version === null || version.eq(content.version)) {
if (!content || version === null || version != content.version) {
this.contentFormCompare = null;
this.contentVersion = null;
this.loadContent(content?.data || {}, true);

4
frontend/src/app/features/content/pages/contents/contents-page.component.ts

@ -13,7 +13,7 @@ import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { Observable } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, switchMap, take, tap } from 'rxjs/operators';
import { AppLanguageDto, AppsState, ConfirmClickDirective, ContentDto, ContentListCellDirective, ContentListCellResizeDirective, ContentListHeaderComponent, ContentListWidthDirective, ContentsService, ContentsState, ContentStatusComponent, contentsTranslationStatus, ContributorsState, defined, DropdownMenuComponent, getTableConfig, KeysPipe, LanguageSelectorComponent, LanguagesState, LayoutComponent, ListViewComponent, LocalStoreService, ModalDirective, ModalModel, ModalPlacementDirective, NotifoComponent, PagerComponent, Queries, Query, QuerySynchronizer, Router2State, SchemaDto, SchemasService, SchemasState, SearchFormComponent, Settings, ShortcutDirective, SidebarMenuDirective, Subscriptions, switchSafe, SyncWidthDirective, TableSettings, TempService, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe, TranslationStatus, UIState } from '@app/shared';
import { AppLanguageDto, AppsState, ConfirmClickDirective, ContentDto, ContentListCellDirective, ContentListCellResizeDirective, ContentListHeaderComponent, ContentListWidthDirective, ContentsService, ContentsState, ContentStatusComponent, contentsTranslationStatus, ContributorsState, defined, DropdownMenuComponent, getTableConfig, KeysPipe, LanguageSelectorComponent, LanguagesState, LayoutComponent, ListViewComponent, LocalStoreService, ModalDirective, ModalModel, ModalPlacementDirective, NotifoComponent, PagerComponent, Queries, Query, QuerySynchronizer, Router2State, SchemaDto, SchemasService, SchemasState, SearchFormComponent, Settings, ShortcutDirective, SidebarMenuDirective, Subscriptions, switchSafe, SyncWidthDirective, TableSettings, TempService, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe, TranslationStatuses, UIState } from '@app/shared';
import { DueTimeSelectorComponent } from '../../shared/due-time-selector.component';
import { ContentComponent } from '../../shared/list/content.component';
import { CustomViewEditorComponent } from './custom-view-editor.component';
@ -82,7 +82,7 @@ export class ContentsPageComponent implements OnInit {
public language!: AppLanguageDto;
public languages!: ReadonlyArray<AppLanguageDto>;
public translationStatus?: TranslationStatus;
public translationStatus?: TranslationStatuses;
public get disableScheduler() {
return this.appsState.snapshot.selectedSettings?.hideScheduler === true;

12
frontend/src/app/features/content/shared/due-time-selector.component.ts

@ -8,7 +8,7 @@
import { booleanAttribute, Component, Input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Observable, of, Subject } from 'rxjs';
import { DateTimeEditorComponent, DialogModel, FocusOnInitDirective, ModalDialogComponent, ModalDirective, TooltipDirective, TranslatePipe } from '@app/shared';
import { DateTime, DateTimeEditorComponent, DialogModel, FocusOnInitDirective, ModalDialogComponent, ModalDirective, TooltipDirective, TranslatePipe } from '@app/shared';
const OPTION_IMMEDIATELY = 'Immediately';
@ -28,7 +28,7 @@ const OPTION_IMMEDIATELY = 'Immediately';
],
})
export class DueTimeSelectorComponent {
private dueTimeResult?: Subject<string | null>;
private dueTimeResult?: Subject<DateTime | undefined>;
@Input({ transform: booleanAttribute })
public disabled?: boolean | null;
@ -38,13 +38,13 @@ export class DueTimeSelectorComponent {
public dueTimeAction: string | null = '';
public dueTimeMode = OPTION_IMMEDIATELY;
public selectDueTime(action: string): Observable<string | null> {
public selectDueTime(action: string): Observable<DateTime | undefined> {
if (this.disabled) {
return of(null);
return of(undefined);
}
this.dueTimeAction = action;
this.dueTimeResult = new Subject<string | null>();
this.dueTimeResult = new Subject<DateTime | undefined>();
this.dueTimeDialog.show();
return this.dueTimeResult;
@ -53,7 +53,7 @@ export class DueTimeSelectorComponent {
public confirmStatusChange() {
const result = this.dueTimeMode === OPTION_IMMEDIATELY ? null : this.dueTime;
this.dueTimeResult?.next(result);
this.dueTimeResult?.next(result ? DateTime.parseISO(result) : undefined);
this.dueTimeResult?.complete();
this.cancelStatusChange();

2
frontend/src/app/features/content/shared/forms/array-editor.component.ts

@ -89,7 +89,7 @@ export class ArrayEditorComponent {
public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes.formModel) {
const maxItems = (this.formModel.field.properties as any)['maxItems'] || Number.MAX_VALUE;
const maxItems = this.formModel.field.rawProperties['maxItems'] || Number.MAX_VALUE;
if (Types.is(this.formModel.field.properties, ComponentsFieldPropertiesDto)) {
this.schemasList = this.formModel.field.properties.schemaIds?.map(x => this.formModel.globals.schemas[x]).defined().sortedByString(x => x.displayName) || [];

6
frontend/src/app/features/content/shared/forms/array-item.component.ts

@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output, QueryList, ViewChildren } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FormHintComponent, IfOnceDirective, invalid$, ObjectFormBase, RootFieldDto, TooltipDirective, TranslatePipe, TypedSimpleChanges, Types, valueProjection$ } from '@app/shared';
import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FormHintComponent, IfOnceDirective, invalid$, NestedFieldDto, ObjectFormBase, TooltipDirective, TranslatePipe, TypedSimpleChanges, Types, valueProjection$ } from '@app/shared';
import { ComponentSectionComponent } from './component-section.component';
@Component({
@ -152,7 +152,7 @@ function getTitle(formModel: ObjectFormBase) {
let valueLength = 0;
function addFields(fields: ReadonlyArray<FieldDto>) {
function addFields(fields: ReadonlyArray<FieldDto | NestedFieldDto>) {
for (const field of fields) {
const fieldValue = value[field.name];
@ -178,7 +178,7 @@ function getTitle(formModel: ObjectFormBase) {
valueLength += formatted.length;
addFields(formModel.schema.fields);
} else if (Types.is(formModel.field, RootFieldDto)) {
} else if (Types.is(formModel.field, FieldDto) && formModel.field.nested) {
addFields(formModel.field.nested);
}

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

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, EventEmitter, HostBinding, inject, Input, numberAttribute, Optional, Output } from '@angular/core';
import { Observable } from 'rxjs';
import { AppLanguageDto, AppsState, changed$, CommentsState, disabled$, EditContentForm, FieldForm, FocusMarkerComponent, invalid$, LocalStoreService, SchemaDto, Settings, TooltipDirective, TranslationsService, TypedSimpleChanges, UIOptions } from '@app/shared';
import { AppLanguageDto, AppsState, changed$, CommentsState, disabled$, EditContentForm, FieldForm, FocusMarkerComponent, invalid$, LocalStoreService, SchemaDto, Settings, TooltipDirective, TranslateDto, TranslationsService, TypedSimpleChanges, UIOptions } from '@app/shared';
import { FieldCopyButtonComponent } from './field-copy-button.component';
import { FieldEditorComponent } from './field-editor.component';
import { FieldLanguagesComponent } from './field-languages.component';
@ -158,7 +158,11 @@ export class ContentFieldComponent {
return;
}
const request = { text, sourceLanguage, targetLanguage };
const request = new TranslateDto({
sourceLanguage,
text,
targetLanguage,
});
this.translations.translate(this.appsState.appName, request)
.subscribe(result => {

4
frontend/src/app/features/content/shared/forms/content-section.component.ts

@ -7,7 +7,7 @@
import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, forwardRef, Input, numberAttribute, Output } from '@angular/core';
import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, FormHintComponent, LocalStoreService, MarkdownDirective, RootFieldDto, SchemaDto, Settings, StatefulComponent, TypedSimpleChanges } from '@app/shared';
import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, FormHintComponent, LocalStoreService, MarkdownDirective, FieldDto, SchemaDto, Settings, StatefulComponent, TypedSimpleChanges } from '@app/shared';
import { ContentFieldComponent } from './content-field.component';
interface State {
@ -48,7 +48,7 @@ export class ContentSectionComponent extends StatefulComponent<State> {
public formContext!: any;
@Input({ required: true })
public formSection!: FieldSection<RootFieldDto, FieldForm>;
public formSection!: FieldSection<FieldDto, FieldForm>;
@Input({ required: true })
public schema!: SchemaDto;

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

@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, ElementRef, EventEmitter, Input, numberAttribute, Output, ViewChild } from '@angular/core';
import { AbstractControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Observable } from 'rxjs';
import { AbstractContentForm, AnnotationCreate, AnnotationsSelect, AppLanguageDto, ChatDialogComponent, CheckboxGroupComponent, CodeEditorComponent, ColorPickerComponent, CommentsState, ConfirmClickDirective, ControlErrorsComponent, DateTimeEditorComponent, DialogModel, disabled$, EditContentForm, FieldDto, FormHintComponent, GeolocationEditorComponent, hasNoValue$, HTTP, IndeterminateValueDirective, MarkdownDirective, MathHelper, MessageBus, ModalDirective, RadioGroupComponent, ReferenceInputComponent, RichEditorComponent, StarsComponent, TagEditorComponent, ToggleComponent, TooltipDirective, TransformInputDirective, TypedSimpleChanges, Types } from '@app/shared';
import { AbstractContentForm, AnnotationCreate, AnnotationsSelect, AnyFieldDto, AppLanguageDto, ChatDialogComponent, CheckboxGroupComponent, CodeEditorComponent, ColorPickerComponent, CommentsState, ConfirmClickDirective, ControlErrorsComponent, DateTimeEditorComponent, DialogModel, disabled$, EditContentForm, FormHintComponent, GeolocationEditorComponent, hasNoValue$, HTTP, IndeterminateValueDirective, MarkdownDirective, MathHelper, MessageBus, ModalDirective, RadioGroupComponent, ReferenceInputComponent, RichEditorComponent, StarsComponent, TagEditorComponent, ToggleComponent, TooltipDirective, TransformInputDirective, TypedSimpleChanges, Types } from '@app/shared';
import { ReferenceDropdownComponent } from '../references/reference-dropdown.component';
import { ReferencesCheckboxesComponent } from '../references/references-checkboxes.component';
import { ReferencesEditorComponent } from '../references/references-editor.component';
@ -84,7 +84,7 @@ export class FieldEditorComponent {
public formLevel!: number;
@Input({ required: true })
public formModel!: AbstractContentForm<FieldDto, AbstractControl>;
public formModel!: AbstractContentForm<AnyFieldDto, AbstractControl>;
@Input({ required: true })
public language!: AppLanguageDto;
@ -142,17 +142,18 @@ export class FieldEditorComponent {
public reset() {
const editor = this.editor as any;
if (!editor) {
return;
}
if (editor) {
const nativeElement = this.editor.nativeElement;
const nativeElement = this.editor.nativeElement;
if (nativeElement && Types.isFunction(nativeElement['reset'])) {
nativeElement['reset']();
}
if (nativeElement && Types.isFunction(nativeElement['reset'])) {
nativeElement['reset']();
}
if (this.editor && Types.isFunction(editor['reset'])) {
editor['reset']();
}
if (this.editor && Types.isFunction(editor['reset'])) {
editor['reset']();
}
}

27
frontend/src/app/features/content/shared/list/content.component.ts

@ -111,22 +111,23 @@ export class ContentComponent {
}
const value = this.patchForm.submit();
if (!value) {
return;
}
if (value) {
this.contentsState.patch(this.content, value)
.subscribe({
next: () => {
this.patchForm!.submitCompleted({ noReset: true });
this.contentsState.patch(this.content, value)
.subscribe({
next: () => {
this.patchForm!.submitCompleted({ noReset: true });
this.changeDetector.detectChanges();
},
error: error => {
this.patchForm!.submitFailed(error);
this.changeDetector.detectChanges();
},
error: error => {
this.patchForm!.submitFailed(error);
this.changeDetector.detectChanges();
},
});
}
this.changeDetector.detectChanges();
},
});
}
public shouldStop(field: TableField) {

36
frontend/src/app/features/content/shared/references/content-creator.component.ts

@ -106,26 +106,26 @@ export class ContentCreatorComponent implements OnInit {
private saveContent(publish: boolean) {
const value = this.contentForm.submit();
if (value) {
if (!this.canCreate(publish)) {
return;
}
this.contentsState.create(value, publish)
.subscribe({
next: content => {
this.contentForm.submitCompleted({ noReset: true });
this.emitSelect(content);
},
error: error => {
this.contentForm.submitFailed(error);
},
});
} else {
if (!value) {
this.contentForm.submitFailed('i18n:contents.contentNotValid');
return;
}
if (!this.canCreate(publish)) {
return;
}
this.contentsState.create(value, publish)
.subscribe({
next: content => {
this.contentForm.submitCompleted({ noReset: true });
this.emitSelect(content);
},
error: error => {
this.contentForm.submitFailed(error);
},
});
}
private canCreate(publish: boolean) {

6
frontend/src/app/features/content/shared/references/reference-dropdown.component.ts

@ -8,7 +8,7 @@
import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { ContentDto, ContentsDto, DropdownComponent, getContentValue, HighlightPipe, LanguageDto, LocalizerService, ResolveContents, SafeHtmlPipe, StatefulControlComponent, Subscriptions, TypedSimpleChanges, Types, value$ } from '@app/shared';
import { AppLanguageDto, ContentDto, ContentsDto, DropdownComponent, getContentValue, HighlightPipe, LocalizerService, ResolveContents, SafeHtmlPipe, StatefulControlComponent, Subscriptions, TypedSimpleChanges, Types, value$ } from '@app/shared';
export const SQX_REFERENCE_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferenceDropdownComponent), multi: true,
@ -53,10 +53,10 @@ export class ReferenceDropdownComponent extends StatefulControlComponent<State,
private isLoadingFailed = false;
@Input({ required: true })
public language!: LanguageDto;
public language!: AppLanguageDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ required: true })
public schemaId!: string;

6
frontend/src/app/features/content/shared/references/reference-item.component.ts

@ -10,7 +10,7 @@
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ConfirmClickDirective, ContentDto, ContentListCellDirective, ContentListFieldComponent, ContentValueComponent, getContentValue, LanguageDto, META_FIELDS, TooltipDirective, TranslatePipe } from '@app/shared';
import { AppLanguageDto, ConfirmClickDirective, ContentDto, ContentListCellDirective, ContentListFieldComponent, ContentValueComponent, getContentValue, META_FIELDS, TooltipDirective, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
@ -38,10 +38,10 @@ export class ReferenceItemComponent {
public clone = new EventEmitter();
@Input()
public language!: LanguageDto;
public language!: AppLanguageDto;
@Input()
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public canRemove?: boolean | null = true;

4
frontend/src/app/features/content/shared/references/references-checkboxes.component.ts

@ -7,7 +7,7 @@
import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, inject, Input } from '@angular/core';
import { FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { AppsState, CheckboxGroupComponent, ContentDto, ContentsService, LanguageDto, LocalizerService, StatefulControlComponent, Subscriptions, TypedSimpleChanges, UIOptions } from '@app/shared';
import { AppLanguageDto, AppsState, CheckboxGroupComponent, ContentDto, ContentsService, LocalizerService, StatefulControlComponent, Subscriptions, TypedSimpleChanges, UIOptions } from '@app/shared';
import { ReferencesTagsConverter } from './references-tag-converter';
export const SQX_REFERENCES_CHECKBOXES_CONTROL_VALUE_ACCESSOR: any = {
@ -45,7 +45,7 @@ export class ReferencesCheckboxesComponent extends StatefulControlComponent<Stat
public schemaId: string | undefined | null;
@Input({ required: true })
public language!: LanguageDto;
public language!: AppLanguageDto;
@Input({ transform: booleanAttribute })
public set disabled(value: boolean | undefined | null) {

4
frontend/src/app/features/content/shared/references/references-radio-buttons.component.ts

@ -7,7 +7,7 @@
import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, inject, Input } from '@angular/core';
import { FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { AppsState, ContentDto, ContentsService, LanguageDto, LocalizerService, RadioGroupComponent, StatefulControlComponent, Subscriptions, TypedSimpleChanges, UIOptions } from '@app/shared';
import { AppLanguageDto, AppsState, ContentDto, ContentsService, LocalizerService, RadioGroupComponent, StatefulControlComponent, Subscriptions, TypedSimpleChanges, UIOptions } from '@app/shared';
import { ReferencesTagsConverter } from './references-tag-converter';
export const SQX_REFERENCES_RADIO_BUTTONS_CONTROL_VALUE_ACCESSOR: any = {
@ -45,7 +45,7 @@ export class ReferencesRadioButtonsComponent extends StatefulControlComponent<St
public schemaId: string | undefined | null;
@Input({ required: true })
public language!: LanguageDto;
public language!: AppLanguageDto;
@Input({ transform: booleanAttribute })
public set disabled(value: boolean | undefined | null) {

6
frontend/src/app/features/content/shared/references/references-tag-converter.ts

@ -5,12 +5,12 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ContentDto, getContentValue, LanguageDto, LocalizerService, TagConverter, TagValue } from '@app/shared/internal';
import { AppLanguageDto, ContentDto, getContentValue, LocalizerService, TagConverter, TagValue } from '@app/shared/internal';
export class ReferencesTagsConverter implements TagConverter {
public tags: ReadonlyArray<TagValue> = [];
constructor(language: LanguageDto, contents: ReadonlyArray<ContentDto>,
constructor(language: AppLanguageDto, contents: ReadonlyArray<ContentDto>,
private readonly localizer: LocalizerService,
) {
this.tags = this.createTags(language, contents);
@ -28,7 +28,7 @@ export class ReferencesTagsConverter implements TagConverter {
return result || null;
}
private createTags(language: LanguageDto, contents: ReadonlyArray<ContentDto>): ReadonlyArray<TagValue> {
private createTags(language: AppLanguageDto, contents: ReadonlyArray<ContentDto>): ReadonlyArray<TagValue> {
if (contents.length === 0) {
return [];
}

6
frontend/src/app/features/content/shared/references/references-tags.component.ts

@ -8,7 +8,7 @@
import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { ContentDto, ContentsDto, LanguageDto, LocalizerService, ResolveContents, StatefulControlComponent, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, Types } from '@app/shared';
import { AppLanguageDto, ContentDto, ContentsDto, LocalizerService, ResolveContents, StatefulControlComponent, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, Types } from '@app/shared';
import { ReferencesTagsConverter } from './references-tag-converter';
export const SQX_REFERENCES_TAGS_CONTROL_VALUE_ACCESSOR: any = {
@ -51,10 +51,10 @@ export class ReferencesTagsComponent extends StatefulControlComponent<State, Rea
public schemaId!: string;
@Input({ required: true })
public language!: LanguageDto;
public language!: AppLanguageDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public set disabled(value: boolean | undefined | null) {

4
frontend/src/app/features/rules/pages/rule/rule-page.component.html

@ -101,7 +101,7 @@
[triggerForm]="currentTrigger"></sqx-content-changed-trigger>
}
@case ("SchemaChanged") {
<sqx-schema-changed-trigger [triggerForm]="currentTrigger.form"></sqx-schema-changed-trigger>
<sqx-schema-changed-trigger [triggerForm]="currentTrigger"></sqx-schema-changed-trigger>
}
@case ("Usage") {
<sqx-usage-trigger [triggerForm]="currentTrigger"></sqx-usage-trigger>
@ -162,7 +162,7 @@
<sqx-generic-action
[actionForm]="currentAction"
[appName]="rulesState.appName"
[trigger]="currentTrigger?.form.value"
[trigger]="currentTrigger?.form?.value || {}"
[triggerType]="currentTrigger?.triggerType"></sqx-generic-action>
} @else {
<div class="row g-0">

24
frontend/src/app/features/rules/pages/rule/rule-page.component.ts

@ -10,7 +10,7 @@ import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { debounceTime, Subscription } from 'rxjs';
import { ActionForm, ALL_TRIGGERS, ConfirmClickDirective, FormAlertComponent, KeysPipe, LayoutComponent, ListViewComponent, MessageBus, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, SidebarMenuDirective, Subscriptions, TitleComponent, ToggleComponent, TooltipDirective, TourHintDirective, TourStepDirective, TranslatePipe, TriggerForm, TriggerType, value$ } from '@app/shared';
import { ActionForm, ALL_TRIGGERS, ConfirmClickDirective, DynamicCreateRuleDto, DynamicRuleDto, DynamicUpdateRuleDto, FormAlertComponent, KeysPipe, LayoutComponent, ListViewComponent, MessageBus, RuleElementDto, RulesService, RulesState, SchemasState, SidebarMenuDirective, Subscriptions, TitleComponent, ToggleComponent, TooltipDirective, TourHintDirective, TourStepDirective, TranslatePipe, TriggerForm, value$ } from '@app/shared';
import { GenericActionComponent } from '../../shared/actions/generic-action.component';
import { RuleElementComponent } from '../../shared/rule-element.component';
import { AssetChangedTriggerComponent } from '../../shared/triggers/asset-changed-trigger.component';
@ -60,7 +60,7 @@ export class RulePageComponent implements OnInit {
public supportedTriggers = ALL_TRIGGERS;
public supportedActions: { [name: string]: RuleElementDto } = {};
public rule?: RuleDto | null;
public rule?: DynamicRuleDto | null;
public currentTrigger?: TriggerForm;
public currentAction?: ActionForm;
@ -69,7 +69,7 @@ export class RulePageComponent implements OnInit {
public isEditable = false;
public get isManual() {
return this.rule?.triggerType === 'Manual';
return this.rule?.trigger.triggerType === 'Manual';
}
public get actionElement() {
@ -77,7 +77,7 @@ export class RulePageComponent implements OnInit {
}
public get triggerElement() {
return this.supportedTriggers[(this.currentTrigger?.triggerType || '') as TriggerType];
return this.supportedTriggers[this.currentTrigger!.triggerType];
}
constructor(
@ -94,7 +94,6 @@ export class RulePageComponent implements OnInit {
this.rulesService.getActions()
.subscribe(actions => {
this.supportedActions = actions;
this.initFromRule();
});
@ -102,7 +101,6 @@ export class RulePageComponent implements OnInit {
this.rulesState.selectedRule
.subscribe(rule => {
this.rule = rule;
this.initFromRule();
}));
@ -114,8 +112,8 @@ export class RulePageComponent implements OnInit {
this.isEditable = this.rule.canUpdate;
this.isEnabled = this.rule.isEnabled;
this.selectAction(this.rule.actionType, this.rule.action);
this.selectTrigger(this.rule.triggerType, this.rule.trigger);
this.selectAction(this.rule.action.actionType, this.rule.action);
this.selectTrigger(this.rule.trigger.triggerType, this.rule.trigger);
} else {
this.isEditable = true;
this.isEnabled = false;
@ -140,7 +138,7 @@ export class RulePageComponent implements OnInit {
}
}
public selectTrigger(type: TriggerType, values?: any) {
public selectTrigger(type: string, values?: any) {
if (this.currentTrigger?.triggerType !== type) {
this.currentTrigger = new TriggerForm(type);
this.currentTrigger.setEnabled(this.isEditable);
@ -175,20 +173,18 @@ export class RulePageComponent implements OnInit {
}
const action = this.currentAction.submit();
if (!action) {
return;
}
const trigger = this.currentTrigger.submit();
if (!trigger || !action) {
return;
}
const request: any = { trigger, action, isEnabled: this.isEnabled };
if (this.rule) {
const request = new DynamicUpdateRuleDto({ trigger, action, isEnabled: this.isEnabled });
this.rulesState.update(this.rule, request)
.subscribe({
next: () => {
@ -199,6 +195,8 @@ export class RulePageComponent implements OnInit {
},
});
} else {
const request = new DynamicCreateRuleDto({ trigger, action });
this.rulesState.create(request)
.subscribe({
next: rule => {

6
frontend/src/app/features/rules/pages/rules/rule.component.html

@ -89,7 +89,7 @@
<h3>{{ "rules.ruleSyntax.if" | sqxTranslate }}</h3>
</div>
<div class="col">
<sqx-rule-element disabled="true" [element]="ruleTriggers[rule.triggerType]" [type]="rule.triggerType"></sqx-rule-element>
<sqx-rule-element disabled="true" [element]="ruleTriggers[rule.trigger.triggerType]" [type]="rule.trigger.triggerType"></sqx-rule-element>
</div>
<div class="col-auto">
<h3>{{ "rules.ruleSyntax.then" | sqxTranslate }}</h3>
@ -97,8 +97,8 @@
<div class="col">
<sqx-rule-element
disabled="true"
[element]="$any(ruleActions[rule.actionType])"
[type]="rule.actionType"></sqx-rule-element>
[element]="$any(ruleActions[rule.action.actionType])"
[type]="rule.action.actionType"></sqx-rule-element>
</div>
<div class="col col-last text-end">
@if (isManual) {

14
frontend/src/app/features/rules/pages/rules/rule.component.ts

@ -9,7 +9,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { ActionsDto, ConfirmClickDirective, DropdownMenuComponent, EditableTitleComponent, ModalDirective, ModalModel, ModalPlacementDirective, RuleDto, RulesState, ToggleComponent, TranslatePipe, TriggersDto } from '@app/shared';
import { ActionsDto, ConfirmClickDirective, DropdownMenuComponent, DynamicRuleDto, DynamicUpdateRuleDto, EditableTitleComponent, ModalDirective, ModalModel, ModalPlacementDirective, RulesState, ToggleComponent, TranslatePipe, TriggersDto } from '@app/shared';
import { RuleElementComponent } from '../../shared/rule-element.component';
@Component({
@ -39,12 +39,12 @@ export class RuleComponent {
public ruleActions!: ActionsDto;
@Input({ required: true })
public rule!: RuleDto;
public rule!: DynamicRuleDto;
public dropdown = new ModalModel();
public get isManual() {
return this.rule.triggerType === 'Manual';
return this.rule.trigger.triggerType === 'Manual';
}
constructor(
@ -65,19 +65,19 @@ export class RuleComponent {
}
public rename(name: string) {
this.rulesState.update(this.rule, { name });
this.rulesState.update(this.rule, new DynamicUpdateRuleDto({ name }));
}
public disable() {
this.rulesState.update(this.rule, { isEnabled: false });
this.rulesState.update(this.rule, new DynamicUpdateRuleDto({ isEnabled: false }));
}
public enable() {
this.rulesState.update(this.rule, { isEnabled: true });
this.rulesState.update(this.rule, new DynamicUpdateRuleDto({ isEnabled: true }));
}
public toggle() {
this.rulesState.update(this.rule, { isEnabled: !this.rule.isEnabled });
this.rulesState.update(this.rule, new DynamicUpdateRuleDto({ isEnabled: !this.rule.isEnabled }));
}
public trigger() {

8
frontend/src/app/features/rules/pages/rules/rules-page.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { ALL_TRIGGERS, LayoutComponent, ListViewComponent, RuleDto, RuleElementDto, RulesService, RulesState, SchemasState, ShortcutDirective, SidebarMenuDirective, TitleComponent, TooltipDirective, TourHintDirective, TourStepDirective, TranslatePipe } from '@app/shared';
import { ALL_TRIGGERS, DynamicRuleDto, DynamicUpdateRuleDto, LayoutComponent, ListViewComponent, RuleElementDto, RulesService, RulesState, SchemasState, ShortcutDirective, SidebarMenuDirective, TitleComponent, TooltipDirective, TourHintDirective, TourStepDirective, TranslatePipe } from '@app/shared';
import { RuleComponent } from './rule.component';
@Component({
@ -63,11 +63,11 @@ export class RulesPageComponent implements OnInit {
this.rulesState.runCancel();
}
public delete(rule: RuleDto) {
public delete(rule: DynamicRuleDto) {
this.rulesState.delete(rule);
}
public toggle(rule: RuleDto) {
this.rulesState.update(rule, { isEnabled: !rule.isEnabled });
public toggle(rule: DynamicRuleDto) {
this.rulesState.update(rule, new DynamicUpdateRuleDto({ isEnabled: !rule.isEnabled }));
}
}

2
frontend/src/app/features/rules/shared/actions/generic-action.component.html

@ -31,7 +31,7 @@
<textarea class="form-control" id="{{ property.name }}" [formControlName]="property.name"></textarea>
}
}
@case ("JavaScript") {
@case ("Javascript") {
<sqx-code-editor
[completion]="ruleCompletions | async"
[formControlName]="property.name"

23
frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.ts

@ -51,17 +51,18 @@ export class SchemaEditFormComponent {
}
const value = this.fieldForm.submit();
if (value) {
this.schemasState.update(this.schema, value)
.subscribe({
next: () => {
this.fieldForm.submitCompleted({ noReset: true });
},
error: error => {
this.fieldForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.schemasState.update(this.schema, value)
.subscribe({
next: () => {
this.fieldForm.submitCompleted({ noReset: true });
},
error: error => {
this.fieldForm.submitFailed(error);
},
});
}
}

23
frontend/src/app/features/schemas/pages/schema/export/schema-export-form.component.ts

@ -47,17 +47,18 @@ export class SchemaExportFormComponent {
}
const value = this.synchronizeForm.submit();
if (value) {
this.schemasState.synchronize(this.schema, value)
.subscribe({
next: () => {
this.synchronizeForm.submitCompleted({ noReset: true });
},
error: error => {
this.synchronizeForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.schemasState.synchronize(this.schema, value)
.subscribe({
next: () => {
this.synchronizeForm.submitCompleted({ noReset: true });
},
error: error => {
this.synchronizeForm.submitFailed(error);
},
});
}
}

8
frontend/src/app/features/schemas/pages/schema/fields/field-group.component.ts

@ -8,7 +8,7 @@
import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList } from '@angular/cdk/drag-drop';
import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
import { AppSettingsDto, FieldDto, FieldGroup, LanguageDto, LocalStoreService, RootFieldDto, SchemaDto, Settings, StatefulComponent } from '@app/shared';
import { AppLanguageDto, AppSettingsDto, FieldDto, FieldGroup, LocalStoreService, SchemaDto, Settings, StatefulComponent } from '@app/shared';
import { FieldComponent } from './field.component';
interface State {
@ -33,10 +33,10 @@ export class FieldGroupComponent extends StatefulComponent<State> {
public sorted = new EventEmitter<CdkDragDrop<FieldDto[]>>();
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input()
public parent?: RootFieldDto;
public parent?: FieldDto;
@Input({ required: true })
public settings!: AppSettingsDto;
@ -56,7 +56,7 @@ export class FieldGroupComponent extends StatefulComponent<State> {
public trackByFieldFn: (_index: number, field: FieldDto) => any;
public get hasAnyFields() {
return this.parent ? this.parent.nested.length > 0 : this.schema.fields.length > 0;
return this.parent ? this.parent.nested && this.parent.nested.length > 0 : this.schema.fields.length > 0;
}
constructor(

2
frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html

@ -11,7 +11,7 @@
@if (editForm) {
<form class="edit-form" [formGroup]="editForm.form" (ngSubmit)="save('Close')">
<sqx-field-form
[field]="field"
[field]="editField"
[fieldForm]="editForm.form"
isEditable="true"
[isLocalizable]="isLocalizable"

21
frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common';
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AddFieldForm, AppSettingsDto, ControlErrorsComponent, createProperties, DropdownMenuComponent, EditFieldForm, FieldDto, fieldTypes, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, LanguagesState, ModalDialogComponent, ModalDirective, ModalModel, ModalPlacementDirective, RootFieldDto, SchemaDto, SchemasState, TooltipDirective, TranslatePipe, Types } from '@app/shared';
import { AddFieldForm, AppSettingsDto, ControlErrorsComponent, createProperties, DropdownMenuComponent, EditFieldForm, FieldDto, fieldTypes, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, LanguagesState, ModalDialogComponent, ModalDirective, ModalModel, ModalPlacementDirective, SchemaDto, SchemasState, TooltipDirective, TranslatePipe, Types, UpdateFieldDto } from '@app/shared';
import { FieldFormComponent } from './forms/field-form.component';
@ -48,21 +48,21 @@ export class FieldWizardComponent implements OnInit {
public settings!: AppSettingsDto;
@Input()
public parent: RootFieldDto | null | undefined;
public parent: FieldDto | null | undefined;
@Output()
public dialogClose = new EventEmitter();
public fieldTypes = fieldTypes;
public field!: FieldDto;
public addFieldForm = new AddFieldForm();
public addFieldModal = new ModalModel();
public editField!: FieldDto;
public editForm?: EditFieldForm;
public get isLocalizable() {
return (this.parent && this.parent.isLocalizable) || (this.field as any)['isLocalizable'];
return (this.parent && this.parent.isLocalizable) || (this.editField as any)['isLocalizable'];
}
constructor(
@ -83,7 +83,6 @@ export class FieldWizardComponent implements OnInit {
public addField(navigationMode: SaveNavigationMode) {
const value = this.addFieldForm.submit();
if (!value) {
return;
}
@ -102,10 +101,9 @@ export class FieldWizardComponent implements OnInit {
break;
case 'Edit':
this.field = dto;
this.editForm = new EditFieldForm(this.field.properties);
this.editForm.load(this.field.properties);
this.editField = dto as any;
this.editForm = new EditFieldForm(this.editField.properties);
this.editForm.load(this.editField.properties);
break;
case 'Close':
@ -124,14 +122,13 @@ export class FieldWizardComponent implements OnInit {
}
const value = this.editForm.submit();
if (!value) {
return;
}
const properties = createProperties(this.field.properties.fieldType, value);
const properties = createProperties(this.editField.properties.fieldType as any, value);
this.schemasState.updateField(this.schema, this.field as RootFieldDto, { properties })
this.schemasState.updateField(this.schema, this.editField, new UpdateFieldDto({ properties }))
.subscribe({
next: () => {
switch (navigationMode) {

35
frontend/src/app/features/schemas/pages/schema/fields/field.component.ts

@ -8,7 +8,7 @@
import { booleanAttribute, Component, forwardRef, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppSettingsDto, ConfirmClickDirective, createProperties, DialogModel, DropdownMenuComponent, EditFieldForm, FieldDto, LanguageDto, ModalDirective, ModalModel, ModalPlacementDirective, NestedFieldDto, RootFieldDto, SchemaDto, SchemasState, TooltipDirective, TourStepDirective, TranslatePipe, TypedSimpleChanges } from '@app/shared';
import { AppLanguageDto, AppSettingsDto, ConfirmClickDirective, createProperties, DialogModel, DropdownMenuComponent, EditFieldForm, FieldDto, ModalDirective, ModalModel, ModalPlacementDirective, SchemaDto, SchemasState, TooltipDirective, TourStepDirective, TranslatePipe, TypedSimpleChanges, UpdateFieldDto } from '@app/shared';
import { FieldWizardComponent } from './field-wizard.component';
import { FieldFormComponent } from './forms/field-form.component';
import { SortableFieldListComponent } from './sortable-field-list.component';
@ -35,7 +35,7 @@ import { SortableFieldListComponent } from './sortable-field-list.component';
})
export class FieldComponent {
@Input({ required: true })
public field!: NestedFieldDto | RootFieldDto;
public field!: FieldDto | FieldDto;
@Input({ required: true })
public schema!: SchemaDto;
@ -44,10 +44,10 @@ export class FieldComponent {
public plain = false;
@Input()
public parent?: RootFieldDto;
public parent?: FieldDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ required: true })
public settings!: AppSettingsDto;
@ -118,19 +118,20 @@ export class FieldComponent {
}
const value = this.editForm.submit();
if (value) {
const properties = createProperties(this.field.properties.fieldType, value);
this.schemasState.updateField(this.schema, this.field, { properties })
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
if (!value) {
return;
}
const properties = createProperties(this.field.properties.fieldType as any, value);
this.schemasState.updateField(this.schema, this.field, new UpdateFieldDto({ properties }))
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
}
}

4
frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts

@ -8,7 +8,7 @@
import { booleanAttribute, Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { AppSettingsDto, FieldDto, LanguageDto, SchemaDto, TranslatePipe } from '@app/shared';
import { AppLanguageDto, AppSettingsDto, FieldDto, SchemaDto, TranslatePipe } from '@app/shared';
import { ArrayValidationComponent } from '../types/array-validation.component';
import { AssetsValidationComponent } from '../types/assets-validation.component';
import { BooleanValidationComponent } from '../types/boolean-validation.component';
@ -61,7 +61,7 @@ export class FieldFormValidationComponent {
public settings!: AppSettingsDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public isLocalizable?: boolean | null;

4
frontend/src/app/features/schemas/pages/schema/fields/forms/field-form.component.ts

@ -8,7 +8,7 @@
import { AfterViewInit, booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { AppSettingsDto, FieldDto, LanguageDto, SchemaDto, TranslatePipe } from '@app/shared';
import { AppLanguageDto, AppSettingsDto, FieldDto, SchemaDto, TranslatePipe } from '@app/shared';
import { JsonMoreComponent } from '../types/json-more.component';
import { FieldFormCommonComponent } from './field-form-common.component';
import { FieldFormUIComponent } from './field-form-ui.component';
@ -47,7 +47,7 @@ export class FieldFormComponent implements AfterViewInit {
public settings!: AppSettingsDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public isLocalizable?: boolean | null;

6
frontend/src/app/features/schemas/pages/schema/fields/sortable-field-list.component.ts

@ -8,7 +8,7 @@
import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList, CdkDropListGroup, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { booleanAttribute, Component, EventEmitter, forwardRef, Input, Output } from '@angular/core';
import { AppSettingsDto, FieldDto, FieldGroup, groupFields, LanguageDto, RootFieldDto, SchemaDto } from '@app/shared';
import { AppLanguageDto, AppSettingsDto, FieldDto, FieldGroup, groupFields, SchemaDto } from '@app/shared';
import { FieldGroupComponent } from './field-group.component';
@Component({
@ -29,10 +29,10 @@ export class SortableFieldListComponent {
public sorted = new EventEmitter<ReadonlyArray<FieldDto>>();
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input()
public parent?: RootFieldDto;
public parent?: FieldDto;
@Input({ required: true })
public settings!: AppSettingsDto;

4
frontend/src/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts

@ -8,7 +8,7 @@
import { booleanAttribute, Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { AssetsFieldPropertiesDto, FieldDto, FormHintComponent, LanguageDto, LocalizedInputComponent, TagEditorComponent, TranslatePipe } from '@app/shared';
import { AppLanguageDto, AssetsFieldPropertiesDto, FieldDto, FormHintComponent, LocalizedInputComponent, TagEditorComponent, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
@ -35,7 +35,7 @@ export class AssetsValidationComponent {
public properties!: AssetsFieldPropertiesDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public isLocalizable?: boolean | null;

4
frontend/src/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts

@ -8,7 +8,7 @@
import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { BOOLEAN_FIELD_EDITORS, BooleanFieldPropertiesDto, FieldDto, FormHintComponent, TranslatePipe } from '@app/shared';
import { BooleanFieldEditorValues, BooleanFieldPropertiesDto, FieldDto, FormHintComponent, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
@ -23,7 +23,7 @@ import { BOOLEAN_FIELD_EDITORS, BooleanFieldPropertiesDto, FieldDto, FormHintCom
],
})
export class BooleanUIComponent {
public readonly editors = BOOLEAN_FIELD_EDITORS;
public readonly editors = BooleanFieldEditorValues;
@Input({ required: true })
public fieldForm!: UntypedFormGroup;

4
frontend/src/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts

@ -9,7 +9,7 @@
import { booleanAttribute, Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { BooleanFieldPropertiesDto, FieldDto, FormHintComponent, hasNoValue$, IndeterminateValueDirective, LanguageDto, LocalizedInputComponent, TranslatePipe, TypedSimpleChanges } from '@app/shared';
import { AppLanguageDto, BooleanFieldPropertiesDto, FieldDto, FormHintComponent, hasNoValue$, IndeterminateValueDirective, LocalizedInputComponent, TranslatePipe, TypedSimpleChanges } from '@app/shared';
@Component({
standalone: true,
@ -36,7 +36,7 @@ export class BooleanValidationComponent {
public properties!: BooleanFieldPropertiesDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public isLocalizable?: boolean | null;

4
frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts

@ -8,7 +8,7 @@
import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { DATETIME_FIELD_EDITORS, DateTimeFieldPropertiesDto, FieldDto, FloatConverter, FormHintComponent, MarkdownDirective, TranslatePipe } from '@app/shared';
import { DateTimeFieldEditorValues, DateTimeFieldPropertiesDto, FieldDto, FloatConverter, FormHintComponent, MarkdownDirective, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
@ -25,7 +25,7 @@ import { DATETIME_FIELD_EDITORS, DateTimeFieldPropertiesDto, FieldDto, FloatConv
})
export class DateTimeUIComponent {
public readonly converter = FloatConverter.INSTANCE;
public readonly editors = DATETIME_FIELD_EDITORS;
public readonly editors = DateTimeFieldEditorValues;
@Input({ required: true })
public fieldForm!: UntypedFormGroup;

4
frontend/src/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts

@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { DateTimeEditorComponent, DateTimeFieldPropertiesDto, FieldDto, FormHintComponent, hasNoValue$, LanguageDto, LocalizedInputComponent, TranslatePipe, TypedSimpleChanges } from '@app/shared';
import { AppLanguageDto, DateTimeEditorComponent, DateTimeFieldPropertiesDto, FieldDto, FormHintComponent, hasNoValue$, LocalizedInputComponent, TranslatePipe, TypedSimpleChanges } from '@app/shared';
const CALCULATED_DEFAULT_VALUES: ReadonlyArray<string> = ['Now', 'Today'];
@ -39,7 +39,7 @@ export class DateTimeValidationComponent {
public properties!: DateTimeFieldPropertiesDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public isLocalizable?: boolean | null;

4
frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.ts

@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common';
import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { FieldDto, FloatConverter, FormHintComponent, NUMBER_FIELD_EDITORS, NumberFieldPropertiesDto, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, valueProjection$ } from '@app/shared';
import { FieldDto, FloatConverter, FormHintComponent, NumberFieldEditorValues, NumberFieldPropertiesDto, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, valueProjection$ } from '@app/shared';
@Component({
standalone: true,
@ -29,7 +29,7 @@ export class NumberUIComponent {
private readonly subscriptions = new Subscriptions();
public readonly converter = FloatConverter.INSTANCE;
public readonly editors = NUMBER_FIELD_EDITORS;
public readonly editors = NumberFieldEditorValues;
@Input({ required: true })
public fieldForm!: UntypedFormGroup;

6
frontend/src/app/features/schemas/pages/schema/fields/types/number-validation.component.ts

@ -8,7 +8,7 @@
import { booleanAttribute, Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { FieldDto, FormHintComponent, LanguageDto, LocalizedInputComponent, NumberFieldPropertiesDto, RootFieldDto, SchemaDto, TranslatePipe, Types } from '@app/shared';
import { AppLanguageDto, FieldDto, FormHintComponent, LocalizedInputComponent, NumberFieldPropertiesDto, SchemaDto, TranslatePipe, Types } from '@app/shared';
@Component({
standalone: true,
@ -37,12 +37,12 @@ export class NumberValidationComponent {
public properties!: NumberFieldPropertiesDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public isLocalizable?: boolean | null;
public get showUnique() {
return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component';
return Types.is(this.field, FieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component';
}
}

4
frontend/src/app/features/schemas/pages/schema/fields/types/references-ui.component.ts

@ -8,7 +8,7 @@
import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { FieldDto, FormHintComponent, REFERENCES_FIELD_EDITORS, ReferencesFieldPropertiesDto, TranslatePipe } from '@app/shared';
import { FieldDto, FormHintComponent, ReferencesFieldEditorValues, ReferencesFieldPropertiesDto, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
@ -23,7 +23,7 @@ import { FieldDto, FormHintComponent, REFERENCES_FIELD_EDITORS, ReferencesFieldP
],
})
export class ReferencesUIComponent {
public readonly editors = REFERENCES_FIELD_EDITORS;
public readonly editors = ReferencesFieldEditorValues;
@Input({ required: true })
public fieldForm!: UntypedFormGroup;

4
frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { FieldDto, FormHintComponent, LanguageDto, LocalizedInputComponent, ReferencesFieldPropertiesDto, SchemaTagSource, TagEditorComponent, TranslatePipe } from '@app/shared';
import { AppLanguageDto, FieldDto, FormHintComponent, LocalizedInputComponent, ReferencesFieldPropertiesDto, SchemaTagSource, TagEditorComponent, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
@ -36,7 +36,7 @@ export class ReferencesValidationComponent {
public properties!: ReferencesFieldPropertiesDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public isLocalizable?: boolean | null;

4
frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts

@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common';
import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { AssetFolderDropdownComponent, FieldDto, FormHintComponent, SchemaTagSource, STRING_FIELD_EDITORS, StringFieldPropertiesDto, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, valueProjection$ } from '@app/shared';
import { AssetFolderDropdownComponent, FieldDto, FormHintComponent, SchemaTagSource, StringFieldEditorValues, StringFieldPropertiesDto, Subscriptions, TagEditorComponent, TranslatePipe, TypedSimpleChanges, valueProjection$ } from '@app/shared';
@Component({
standalone: true,
@ -29,7 +29,7 @@ import { AssetFolderDropdownComponent, FieldDto, FormHintComponent, SchemaTagSou
export class StringUIComponent {
private readonly subscriptions = new Subscriptions();
public readonly editors = STRING_FIELD_EDITORS;
public readonly editors = StringFieldEditorValues;
@Input({ required: true })
public fieldForm!: UntypedFormGroup;

8
frontend/src/app/features/schemas/pages/schema/fields/types/string-validation.component.ts

@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { AppSettingsDto, DropdownMenuComponent, FieldDto, FormHintComponent, hasNoValue$, hasValue$, LanguageDto, LocalizedInputComponent, ModalDirective, ModalModel, ModalPlacementDirective, PatternDto, RootFieldDto, SchemaDto, STRING_CONTENT_TYPES, StringFieldPropertiesDto, Subscriptions, TranslatePipe, TypedSimpleChanges, Types, value$ } from '@app/shared';
import { AppLanguageDto, AppSettingsDto, DropdownMenuComponent, FieldDto, FormHintComponent, hasNoValue$, hasValue$, LocalizedInputComponent, ModalDirective, ModalModel, ModalPlacementDirective, PatternDto, SchemaDto, StringContentTypeValues, StringFieldPropertiesDto, Subscriptions, TranslatePipe, TypedSimpleChanges, Types, value$ } from '@app/shared';
@Component({
standalone: true,
@ -31,7 +31,7 @@ import { AppSettingsDto, DropdownMenuComponent, FieldDto, FormHintComponent, has
export class StringValidationComponent {
private readonly subscriptions = new Subscriptions();
public readonly contentTypes = STRING_CONTENT_TYPES;
public readonly contentTypes = StringContentTypeValues;
@Input({ required: true })
public fieldForm!: UntypedFormGroup;
@ -49,7 +49,7 @@ export class StringValidationComponent {
public settings!: AppSettingsDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public isLocalizable?: boolean | null;
@ -61,7 +61,7 @@ export class StringValidationComponent {
public patternsModal = new ModalModel();
public get showUnique() {
return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component';
return Types.is(this.field, FieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component';
}
public ngOnChanges(changes: TypedSimpleChanges<this>) {

4
frontend/src/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts

@ -8,7 +8,7 @@
import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { FieldDto, FormHintComponent, TagEditorComponent, TAGS_FIELD_EDITORS, TagsFieldPropertiesDto, TranslatePipe } from '@app/shared';
import { FieldDto, FormHintComponent, TagEditorComponent, TagsFieldEditorValues, TagsFieldPropertiesDto, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
@ -24,7 +24,7 @@ import { FieldDto, FormHintComponent, TagEditorComponent, TAGS_FIELD_EDITORS, Ta
],
})
export class TagsUIComponent {
public readonly editors = TAGS_FIELD_EDITORS;
public readonly editors = TagsFieldEditorValues;
@Input({ required: true })
public fieldForm!: UntypedFormGroup;

4
frontend/src/app/features/schemas/pages/schema/fields/types/tags-validation.component.ts

@ -8,7 +8,7 @@
import { booleanAttribute, Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { FieldDto, FormHintComponent, LanguageDto, LocalizedInputComponent, TagEditorComponent, TagsFieldPropertiesDto, TranslatePipe } from '@app/shared';
import { AppLanguageDto, FieldDto, FormHintComponent, LocalizedInputComponent, TagEditorComponent, TagsFieldPropertiesDto, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
@ -35,7 +35,7 @@ export class TagsValidationComponent {
public properties!: TagsFieldPropertiesDto;
@Input({ required: true })
public languages!: ReadonlyArray<LanguageDto>;
public languages!: ReadonlyArray<AppLanguageDto>;
@Input({ transform: booleanAttribute })
public isLocalizable?: boolean | null;

25
frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.ts

@ -83,18 +83,19 @@ export class IndexFormComponent {
}
public createSchema() {
const fields = this.createForm.submit();
if (fields) {
this.indexesState.create({ fields })
.subscribe({
next: () => {
this.emitCreate();
},
error: error => {
this.createForm.submitFailed(error);
},
});
const value = this.createForm.submit();
if (!value) {
return;
}
this.indexesState.create(value)
.subscribe({
next: () => {
this.emitCreate();
},
error: error => {
this.createForm.submitFailed(error);
},
});
}
}

23
frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts

@ -60,17 +60,18 @@ export class SchemaPreviewUrlsFormComponent implements OnInit {
}
const value = this.editForm.submit();
if (value) {
this.schemasState.configurePreviewUrls(this.schema, value)
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.schemasState.configurePreviewUrls(this.schema, value)
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
}
}

25
frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts

@ -56,7 +56,7 @@ export class SchemaFieldRulesFormComponent implements OnInit {
if (field.properties.isContentField) {
fieldNames.push(field.name);
for (const nestedField of field.nested) {
for (const nestedField of field.nested || []) {
if (nestedField.properties.isContentField) {
fieldNames.push(`${field.name}.${nestedField.name}`);
}
@ -82,17 +82,18 @@ export class SchemaFieldRulesFormComponent implements OnInit {
}
const value = this.editForm.submit();
if (value) {
this.schemasState.configureFieldRules(this.schema, value)
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.schemasState.configureFieldRules(this.schema, value)
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
}
}

23
frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts

@ -63,17 +63,18 @@ export class SchemaScriptsFormComponent {
}
const value = this.editForm.submit();
if (value) {
this.schemasState.configureScripts(this.schema, value)
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.schemasState.configureScripts(this.schema, value)
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
}
}

2
frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts

@ -39,7 +39,7 @@ export class FieldListComponent {
public withMetaFields = false;
@Output()
public fieldNamesChange = new EventEmitter<ReadonlyArray<string>>();
public fieldNamesChange = new EventEmitter<string[]>();
public fieldsAdded!: TableField[];
public fieldsNotAdded!: TableField[];

14
frontend/src/app/features/schemas/pages/schema/ui/schema-ui-form.component.ts

@ -7,7 +7,7 @@
import { Component, Input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SchemaDto, SchemasState, TranslatePipe } from '@app/shared';
import { ConfigureUIFieldsDto, SchemaDto, SchemasState, TranslatePipe } from '@app/shared';
import { FieldListComponent } from './field-list.component';
@Component({
@ -29,8 +29,8 @@ export class SchemaUIFormComponent {
public isEditable = false;
public fieldsInLists: ReadonlyArray<string> = [];
public fieldsInReferences: ReadonlyArray<string> = [];
public fieldsInLists: string[] = [];
public fieldsInReferences: string[] = [];
constructor(
private readonly schemasState: SchemasState,
@ -44,11 +44,11 @@ export class SchemaUIFormComponent {
this.fieldsInReferences = this.schema.fieldsInReferences;
}
public setFieldsInLists(names: ReadonlyArray<string>) {
public setFieldsInLists(names: string[]) {
this.fieldsInLists = names;
}
public setFieldsInReferences(names: ReadonlyArray<string>) {
public setFieldsInReferences(names: string[]) {
this.fieldsInReferences = names;
}
@ -61,9 +61,11 @@ export class SchemaUIFormComponent {
return;
}
this.schemasState.configureUIFields(this.schema, {
const request = new ConfigureUIFieldsDto({
fieldsInLists: this.fieldsInLists,
fieldsInReferences: this.fieldsInReferences,
});
this.schemasState.configureUIFields(this.schema, request);
}
}

23
frontend/src/app/features/schemas/pages/schemas/schema-form.component.ts

@ -74,17 +74,18 @@ export class SchemaFormComponent implements OnInit {
public createSchema() {
const value = this.createForm.submit();
if (value) {
this.schemasState.create(value)
.subscribe({
next: dto => {
this.emitCreate(dto);
},
error: error => {
this.createForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.schemasState.create(value)
.subscribe({
next: dto => {
this.emitCreate(dto);
},
error: error => {
this.createForm.submitFailed(error);
},
});
}
}

13
frontend/src/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -89,13 +89,14 @@ export class SchemasPageComponent implements OnInit {
public addCategory() {
const value = this.addCategoryForm.submit();
if (!value) {
return;
}
if (value) {
try {
this.schemasState.addCategory(value.name);
} finally {
this.addCategoryForm.submitCompleted();
}
try {
this.schemasState.addCategory(value.name);
} finally {
this.addCategoryForm.submitCompleted();
}
}

23
frontend/src/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts

@ -83,17 +83,18 @@ export class AssetScriptsPageComponent implements OnInit {
}
const value = this.editForm.submit();
if (value) {
this.assetScriptsState.update(value)
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.assetScriptsState.update(value)
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
}
}

23
frontend/src/app/features/settings/pages/clients/client-add-form.component.ts

@ -36,18 +36,19 @@ export class ClientAddFormComponent {
public addClient() {
const value = this.addClientForm.submit();
if (value) {
this.clientsState.attach(value)
.subscribe({
next: () => {
this.addClientForm.submitCompleted();
},
error: error => {
this.addClientForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.clientsState.attach(value)
.subscribe({
next: () => {
this.addClientForm.submitCompleted();
},
error: error => {
this.addClientForm.submitFailed(error);
},
});
}
public cancel() {

2
frontend/src/app/features/settings/pages/clients/client.component.html

@ -124,7 +124,7 @@
</div>
@if (client.canUpdate) {
<div class="col-auto">
<button class="btn btn-outline-secondary" (click)="updateApiCallsLimit()">
<button class="btn btn-outline-secondary" (click)="updateApiCallsLimit(apiCallsLimit)">
{{ "common.save" | sqxTranslate }}
</button>
</div>

20
frontend/src/app/features/settings/pages/clients/client.component.ts

@ -8,7 +8,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppsState, ClientDto, ClientsState, ConfirmClickDirective, CopyDirective, DialogModel, EditableTitleComponent, FormHintComponent, ModalDirective, RoleDto, TooltipDirective, TourStepDirective, TranslatePipe, TypedSimpleChanges } from '@app/shared';
import { AppsState, ClientDto, ClientsState, ConfirmClickDirective, CopyDirective, DialogModel, EditableTitleComponent, FormHintComponent, ModalDirective, RoleDto, TooltipDirective, TourStepDirective, TranslatePipe, TypedSimpleChanges, UpdateClientDto } from '@app/shared';
import { ClientConnectFormComponent } from './client-connect-form.component';
@Component({
@ -58,18 +58,26 @@ export class ClientComponent {
}
public updateRole(role: string) {
this.clientsState.update(this.client, { role });
const request = new UpdateClientDto({ role });
this.clientsState.update(this.client, request);
}
public updateAllowAnonymous(allowAnonymous: boolean) {
this.clientsState.update(this.client, { allowAnonymous });
const request = new UpdateClientDto({ allowAnonymous });
this.clientsState.update(this.client, request);
}
public updateApiCallsLimit() {
this.clientsState.update(this.client, { apiCallsLimit: this.apiCallsLimit });
public updateApiCallsLimit(apiCallsLimit: number) {
const request = new UpdateClientDto({ apiCallsLimit });
this.clientsState.update(this.client, request);
}
public rename(name: string) {
this.clientsState.update(this.client, { name });
const request = new UpdateClientDto({ name });
this.clientsState.update(this.client, request);
}
}

33
frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.ts

@ -85,23 +85,24 @@ export class ContributorAddFormComponent {
public assignContributor() {
const value = this.assignContributorForm.submit();
if (!value) {
return;
}
if (value) {
this.contributorsState.assign(value)
.subscribe({
next: isCreated => {
this.assignContributorForm.submitCompleted({ newValue: this.defaultValue });
this.contributorsState.assign(value)
.subscribe({
next: isCreated => {
this.assignContributorForm.submitCompleted({ newValue: this.defaultValue });
if (isCreated) {
this.dialogs.notifyInfo('i18n:contributors.contributorAssigned');
} else {
this.dialogs.notifyInfo('i18n:contributors.contributorAssignedOld');
}
},
error: error => {
this.assignContributorForm.submitFailed(error);
},
});
}
if (isCreated) {
this.dialogs.notifyInfo('i18n:contributors.contributorAssigned');
} else {
this.dialogs.notifyInfo('i18n:contributors.contributorAssignedOld');
}
},
error: error => {
this.assignContributorForm.submitFailed(error);
},
});
}
}

6
frontend/src/app/features/settings/pages/contributors/contributor.component.ts

@ -10,7 +10,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ConfirmClickDirective, ContributorDto, ContributorsState, HighlightPipe, RoleDto, TooltipDirective, UserPicturePipe } from '@app/shared';
import { AssignContributorDto, ConfirmClickDirective, ContributorDto, ContributorsState, HighlightPipe, RoleDto, TooltipDirective, UserPicturePipe } from '@app/shared';
@Component({
standalone: true,
@ -46,6 +46,8 @@ export class ContributorComponent {
}
public changeRole(role: string) {
this.contributorsState.assign({ contributorId: this.contributor.contributorId, role });
const request = new AssignContributorDto({ contributorId: this.contributor.contributorId, role });
this.contributorsState.assign(request);
}
}

4
frontend/src/app/features/settings/pages/contributors/import-contributors-dialog.component.ts

@ -10,7 +10,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { EMPTY, of } from 'rxjs';
import { catchError, mergeMap, tap } from 'rxjs/operators';
import { ContributorsState, ErrorDto, FormHintComponent, ImportContributorsForm, ModalDialogComponent, RoleDto, StatusIconComponent, TooltipDirective, TranslatePipe } from '@app/shared';
import { AssignContributorDto, ContributorsState, ErrorDto, FormHintComponent, ImportContributorsForm, ModalDialogComponent, RoleDto, StatusIconComponent, TooltipDirective, TranslatePipe } from '@app/shared';
type ImportStatus = {
email: string;
@ -96,7 +96,7 @@ export class ImportContributorsDialogComponent {
}
function createRequest(status: ImportStatus) {
return { contributorId: status.email, role: status.role, invite: true };
return new AssignContributorDto({ contributorId: status.email, role: status.role, invite: true });
}
function getError(error: ErrorDto): string {

23
frontend/src/app/features/settings/pages/languages/language-add-form.component.ts

@ -69,17 +69,18 @@ export class LanguageAddFormComponent {
public addLanguage() {
const value = this.addLanguageForm.submit();
if (value) {
this.languagesState.add(value.language)
.subscribe({
next: () => {
this.addLanguageForm.submitCompleted();
},
error: error => {
this.addLanguageForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.languagesState.add(value.language)
.subscribe({
next: () => {
this.addLanguageForm.submitCompleted();
},
error: error => {
this.addLanguageForm.submitFailed(error);
},
});
}
}

31
frontend/src/app/features/settings/pages/languages/language.component.ts

@ -9,7 +9,7 @@ import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList } from '@angular/cdk/d
import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppLanguageDto, ConfirmClickDirective, EditLanguageForm, FormHintComponent, LanguageDto, LanguagesState, sorted, TranslatePipe } from '@app/shared';
import { AppLanguageDto, ConfirmClickDirective, EditLanguageForm, FormHintComponent, LanguageDto, LanguagesState, sorted, TranslatePipe, UpdateLanguageDto } from '@app/shared';
@Component({
standalone: true,
@ -67,7 +67,7 @@ export class LanguageComponent {
}
public sort(event: CdkDragDrop<ReadonlyArray<AppLanguageDto>>) {
this.fallbackLanguages = sorted(event);
this.fallbackLanguages = sorted(event) as any;
}
public save() {
@ -76,20 +76,21 @@ export class LanguageComponent {
}
const value = this.editForm.submit();
if (value) {
const request = { ...value, fallback: this.fallbackLanguages.map(x => x.iso2Code) };
this.languagesState.update(this.language, request)
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
if (!value) {
return;
}
const request = new UpdateLanguageDto({ ...value, fallback: this.fallbackLanguages.map(x => x.iso2Code) });
this.languagesState.update(this.language, request)
.subscribe({
next: () => {
this.editForm.submitCompleted({ noReset: true });
},
error: error => {
this.editForm.submitFailed(error);
},
});
}
public removeFallbackLanguage(language: LanguageDto) {

54
frontend/src/app/features/settings/pages/more/more-page.component.ts

@ -92,20 +92,21 @@ export class MorePageComponent implements OnInit {
}
const value = this.updateForm.submit();
if (value) {
this.appsState.update(this.app, value)
.subscribe({
next: app => {
this.updateForm.submitCompleted({ newValue: app });
},
error: error => {
this.dialogs.notifyError(error);
this.updateForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.appsState.update(this.app, value)
.subscribe({
next: app => {
this.updateForm.submitCompleted({ newValue: app });
},
error: error => {
this.dialogs.notifyError(error);
this.updateForm.submitFailed(error);
},
});
}
public transfer() {
@ -114,20 +115,21 @@ export class MorePageComponent implements OnInit {
}
const value = this.transferForm.submit();
if (value) {
this.appsState.transfer(this.app, value.teamId)
.subscribe({
next: app => {
this.transferForm.submitCompleted({ newValue: app });
},
error: error => {
this.dialogs.notifyError(error);
this.transferForm.submitFailed(error);
},
});
if (!value) {
return;
}
this.appsState.transfer(this.app, value)
.subscribe({
next: app => {
this.transferForm.submitCompleted({ newValue: app });
},
error: error => {
this.dialogs.notifyError(error);
this.transferForm.submitFailed(error);
},
});
}
public uploadImage(file: ReadonlyArray<File>) {

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save