фронт поправлен

This commit is contained in:
gorbenko.pavel
2026-07-31 12:51:48 +05:00
parent fdd3787e43
commit 06b52c5186
25 changed files with 573 additions and 45 deletions
@@ -1,9 +1,9 @@
namespace Tnsu.ApprovalRoute.Application.RequestTypes;
public sealed record RequestTypeDto(int Id, string Name, string PageUrl, string? ListName, string Section);
public sealed record RequestTypeDto(int Id, string Name, string PageUrl, string? ListName, string? SiteUrl, string Section);
public sealed record CreateRequestTypeRequest(string Name, string PageUrl, string? ListName, string Section);
public sealed record CreateRequestTypeRequest(string Name, string PageUrl, string? ListName, string? SiteUrl, string Section);
public sealed record UpdateRequestTypeRequest(string Name, string PageUrl, string? ListName, string Section);
public sealed record UpdateRequestTypeRequest(string Name, string PageUrl, string? ListName, string? SiteUrl, string Section);
public sealed record PatchRequestTypeRequest(string? Name, string? PageUrl, string? ListName, string? Section);
public sealed record PatchRequestTypeRequest(string? Name, string? PageUrl, string? ListName, string? SiteUrl, string? Section);
@@ -12,7 +12,7 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db)
{
var items = await db.RequestTypes.AsNoTracking()
.OrderBy(x => x.Name)
.Select(x => new RequestTypeDto(x.Id, x.Name, x.PageUrl, x.ListName, x.Section))
.Select(x => new RequestTypeDto(x.Id, x.Name, x.PageUrl, x.ListName, x.SiteUrl, x.Section))
.ToListAsync(cancellationToken);
return Result.Ok<IReadOnlyList<RequestTypeDto>>(items);
@@ -29,11 +29,16 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db)
public async Task<Result<RequestTypeDto>> CreateAsync(CreateRequestTypeRequest request, CancellationToken cancellationToken)
{
if (!RequestTypeFields.IsListReferenceValid(request.ListName, request.SiteUrl))
return Result.Fail<RequestTypeDto>(new ValidationError(
"Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе."));
var entity = new RequestType
{
Name = request.Name,
PageUrl = request.PageUrl,
ListName = request.ListName,
SiteUrl = request.SiteUrl,
Section = request.Section
};
db.RequestTypes.Add(entity);
@@ -48,9 +53,14 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db)
if (entity is null)
return Result.Fail<RequestTypeDto>(new NotFoundError("Тип заявки", id));
if (!RequestTypeFields.IsListReferenceValid(request.ListName, request.SiteUrl))
return Result.Fail<RequestTypeDto>(new ValidationError(
"Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе."));
entity.Name = request.Name;
entity.PageUrl = request.PageUrl;
entity.ListName = request.ListName;
entity.SiteUrl = request.SiteUrl;
entity.Section = request.Section;
await db.SaveChangesAsync(cancellationToken);
@@ -63,9 +73,17 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db)
if (entity is null)
return Result.Fail<RequestTypeDto>(new NotFoundError("Тип заявки", id));
var listName = request.ListName ?? entity.ListName;
var siteUrl = request.SiteUrl ?? entity.SiteUrl;
if (!RequestTypeFields.IsListReferenceValid(listName, siteUrl))
return Result.Fail<RequestTypeDto>(new ValidationError(
"Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе."));
entity.Name = request.Name ?? entity.Name;
entity.PageUrl = request.PageUrl ?? entity.PageUrl;
entity.ListName = request.ListName ?? entity.ListName;
entity.ListName = listName;
entity.SiteUrl = siteUrl;
entity.Section = request.Section ?? entity.Section;
await db.SaveChangesAsync(cancellationToken);
@@ -84,5 +102,5 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db)
return Result.Ok();
}
private static RequestTypeDto ToDto(RequestType e) => new(e.Id, e.Name, e.PageUrl, e.ListName, e.Section);
private static RequestTypeDto ToDto(RequestType e) => new(e.Id, e.Name, e.PageUrl, e.ListName, e.SiteUrl, e.Section);
}
@@ -9,6 +9,9 @@ public sealed class CreateRequestTypeRequestValidator : AbstractValidator<Create
RuleFor(x => x.Name).NotEmpty().WithMessage("Имя типа заявки обязательно.");
RuleFor(x => x.PageUrl).NotEmpty().WithMessage("Url страницы обязателен.");
RuleFor(x => x.Section).NotEmpty().WithMessage("Раздел обязателен.");
RuleFor(x => x)
.Must(x => RequestTypeFields.IsListReferenceValid(x.ListName, x.SiteUrl))
.WithMessage("Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе.");
}
}
@@ -19,6 +22,9 @@ public sealed class UpdateRequestTypeRequestValidator : AbstractValidator<Update
RuleFor(x => x.Name).NotEmpty().WithMessage("Имя типа заявки обязательно.");
RuleFor(x => x.PageUrl).NotEmpty().WithMessage("Url страницы обязателен.");
RuleFor(x => x.Section).NotEmpty().WithMessage("Раздел обязателен.");
RuleFor(x => x)
.Must(x => RequestTypeFields.IsListReferenceValid(x.ListName, x.SiteUrl))
.WithMessage("Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе.");
}
}
@@ -34,3 +40,9 @@ public sealed class PatchRequestTypeRequestValidator : AbstractValidator<PatchRe
RuleFor(x => x.Section).NotEmpty().WithMessage("Раздел не может быть пустым."));
}
}
internal static class RequestTypeFields
{
public static bool IsListReferenceValid(string? listName, string? siteUrl) =>
string.IsNullOrWhiteSpace(listName) == string.IsNullOrWhiteSpace(siteUrl);
}
@@ -6,6 +6,7 @@ public class RequestType
public string Name { get; set; } = string.Empty;
public string PageUrl { get; set; } = string.Empty;
public string? ListName { get; set; }
public string? SiteUrl { get; set; }
public string Section { get; set; } = string.Empty;
public ICollection<TemplateRoute> Routes { get; set; } = new List<TemplateRoute>();
@@ -17,6 +17,7 @@ public class ApprovalRouteDbContext(DbContextOptions<ApprovalRouteDbContext> opt
e.Property(x => x.Name).HasMaxLength(256).IsRequired();
e.Property(x => x.PageUrl).HasMaxLength(512).IsRequired();
e.Property(x => x.ListName).HasMaxLength(256);
e.Property(x => x.SiteUrl).HasMaxLength(512);
e.Property(x => x.Section).HasMaxLength(256).IsRequired();
});
@@ -12,7 +12,7 @@ using Tnsu.ApprovalRoute.Infrastructure.Persistence;
namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(ApprovalRouteDbContext))]
[Migration("20260731062407_AddRequestTypeListNameSectionAndStepConditionFields")]
[Migration("20260731073536_AddRequestTypeListNameSectionAndStepConditionFields")]
partial class AddRequestTypeListNameSectionAndStepConditionFields
{
/// <inheritdoc />
@@ -57,6 +57,11 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
.HasColumnType("character varying(256)")
.HasColumnName("section");
b.Property<string>("SiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("site_url");
b.HasKey("Id")
.HasName("pk_request_types");
@@ -45,6 +45,13 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
maxLength: 256,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "site_url",
table: "request_types",
type: "character varying(512)",
maxLength: 512,
nullable: true);
}
/// <inheritdoc />
@@ -69,6 +76,10 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
migrationBuilder.DropColumn(
name: "section",
table: "request_types");
migrationBuilder.DropColumn(
name: "site_url",
table: "request_types");
}
}
}
@@ -54,6 +54,11 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations
.HasColumnType("character varying(256)")
.HasColumnName("section");
b.Property<string>("SiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("site_url");
b.HasKey("Id")
.HasName("pk_request_types");
@@ -13,7 +13,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I
public async Task CreateAsync_Then_GetAsync_ReturnsCreatedEntity()
{
// Action
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Vacation", "/vacation", null, "General"), CancellationToken.None);
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Vacation", "/vacation", null, null, "General"), CancellationToken.None);
// Assert
created.IsSuccess.ShouldBeTrue();
@@ -43,11 +43,11 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I
public async Task UpdateAsync_ChangesFields()
{
// Arrange
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Old name", "/old", null, "General"), CancellationToken.None);
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Old name", "/old", null, null, "General"), CancellationToken.None);
// Action
var updated = await _service.UpdateAsync(
created.Value.Id, new UpdateRequestTypeRequest("New name", "/new", null, "General"), CancellationToken.None);
created.Value.Id, new UpdateRequestTypeRequest("New name", "/new", null, null, "General"), CancellationToken.None);
// Assert
updated.IsSuccess.ShouldBeTrue();
@@ -60,7 +60,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I
{
// Action
var result = await _service.UpdateAsync(
999999, new UpdateRequestTypeRequest("X", "/x", null, "General"), CancellationToken.None);
999999, new UpdateRequestTypeRequest("X", "/x", null, null, "General"), CancellationToken.None);
// Assert
result.IsFailed.ShouldBeTrue();
@@ -70,11 +70,11 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I
public async Task PatchAsync_UpdatesOnlyProvidedFields()
{
// Arrange
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Original name", "/original", null, "General"), CancellationToken.None);
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Original name", "/original", null, null, "General"), CancellationToken.None);
// Action
var patched = await _service.PatchAsync(
created.Value.Id, new PatchRequestTypeRequest(Name: "Patched name", PageUrl: null, ListName: null, Section: null), CancellationToken.None);
created.Value.Id, new PatchRequestTypeRequest(Name: "Patched name", PageUrl: null, ListName: null, SiteUrl: null, Section: null), CancellationToken.None);
// Assert
patched.IsSuccess.ShouldBeTrue();
@@ -87,7 +87,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I
{
// Action
var result = await _service.PatchAsync(
999999, new PatchRequestTypeRequest(Name: "X", PageUrl: null, ListName: null, Section: null), CancellationToken.None);
999999, new PatchRequestTypeRequest(Name: "X", PageUrl: null, ListName: null, SiteUrl: null, Section: null), CancellationToken.None);
// Assert
result.IsFailed.ShouldBeTrue();
@@ -97,7 +97,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I
public async Task DeleteAsync_RemovesEntity()
{
// Arrange
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Temp", "/temp", null, "General"), CancellationToken.None);
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Temp", "/temp", null, null, "General"), CancellationToken.None);
// Action
var deleted = await _service.DeleteAsync(created.Value.Id, CancellationToken.None);
@@ -109,11 +109,37 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I
fetched.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Создание типа заявки с list_name без site_url возвращает неуспешный результат")]
public async Task CreateAsync_ListNameWithoutSiteUrl_ReturnsFailedResult()
{
// Action
var result = await _service.CreateAsync(
new CreateRequestTypeRequest("Missing site url", "/missing-site-url", "Requests", null, "General"), CancellationToken.None);
// Assert
result.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Создание типа заявки с list_name и site_url успешно")]
public async Task CreateAsync_ListNameWithSiteUrl_ReturnsCreatedEntity()
{
// Action
var created = await _service.CreateAsync(
new CreateRequestTypeRequest(
"With list reference", "/with-list-reference", "Requests", "https://tnsukz.sharepoint.com/sites/hrm", "General"),
CancellationToken.None);
// Assert
created.IsSuccess.ShouldBeTrue();
created.Value.ListName.ShouldBe("Requests");
created.Value.SiteUrl.ShouldBe("https://tnsukz.sharepoint.com/sites/hrm");
}
[Fact(DisplayName = "Список типов заявок включает ранее созданный тип")]
public async Task ListAsync_IncludesCreatedEntity()
{
// Arrange
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Listed type", "/listed", null, "General"), CancellationToken.None);
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Listed type", "/listed", null, null, "General"), CancellationToken.None);
// Action
var list = await _service.ListAsync(CancellationToken.None);
@@ -38,7 +38,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
}
private async Task<int> CreateRequestTypeAsync(string name) =>
(await _requestTypeService.CreateAsync(new CreateRequestTypeRequest(name, $"/{name}", null, "General"), CancellationToken.None)).Value.Id;
(await _requestTypeService.CreateAsync(new CreateRequestTypeRequest(name, $"/{name}", null, null, "General"), CancellationToken.None)).Value.Id;
[Fact(DisplayName = "Создание маршрута и получение его по Id возвращает сохранённые данные")]
public async Task CreateAsync_ThenGetAsync_ReturnsCreatedEntity()
@@ -196,7 +196,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = services.GetRequiredService<TemplateRouteStepService>();
var requestTypeId = (await requestTypeService.CreateAsync(
new CreateRequestTypeRequest("Collapse duplicates request", "/collapse-duplicates", null, "General"), CancellationToken.None)).Value.Id;
new CreateRequestTypeRequest("Collapse duplicates request", "/collapse-duplicates", null, null, "General"), CancellationToken.None)).Value.Id;
var route = await routeService.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None);
@@ -272,7 +272,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = services.GetRequiredService<TemplateRouteStepService>();
var requestTypeId = (await requestTypeService.CreateAsync(
new CreateRequestTypeRequest("Position request", "/position-request", null, "General"), CancellationToken.None)).Value.Id;
new CreateRequestTypeRequest("Position request", "/position-request", null, null, "General"), CancellationToken.None)).Value.Id;
var route = await routeService.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None);
@@ -311,7 +311,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = services.GetRequiredService<TemplateRouteStepService>();
var requestTypeId = (await requestTypeService.CreateAsync(
new CreateRequestTypeRequest("Chief request", "/chief-request", null, "General"), CancellationToken.None)).Value.Id;
new CreateRequestTypeRequest("Chief request", "/chief-request", null, null, "General"), CancellationToken.None)).Value.Id;
var route = await routeService.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None);
@@ -346,7 +346,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
var stepService = services.GetRequiredService<TemplateRouteStepService>();
var requestTypeId = (await requestTypeService.CreateAsync(
new CreateRequestTypeRequest("DirectorTansu request", "/director-tansu-request", null, "General"), CancellationToken.None)).Value.Id;
new CreateRequestTypeRequest("DirectorTansu request", "/director-tansu-request", null, null, "General"), CancellationToken.None)).Value.Id;
var route = await routeService.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
CancellationToken.None);
@@ -26,8 +26,9 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
private async Task<int> CreateRouteAsync(string requestTypeName, bool byProject, string? listName = null)
{
var siteUrl = listName is null ? null : "https://example.sharepoint.com/sites/test";
var requestTypeId = (await _requestTypeService.CreateAsync(
new CreateRequestTypeRequest(requestTypeName, $"/{requestTypeName}", listName, "General"), CancellationToken.None)).Value.Id;
new CreateRequestTypeRequest(requestTypeName, $"/{requestTypeName}", listName, siteUrl, "General"), CancellationToken.None)).Value.Id;
var route = await _routeService.CreateAsync(
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, null, CompanyId, byProject, RequiresControl: false),