diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs index 59b6536..bc44c5a 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeDto.cs @@ -1,9 +1,9 @@ namespace Tnsu.ApprovalRoute.Application.RequestTypes; -public sealed record RequestTypeDto(int Id, string Name, string PageUrl, string? ListName, string? SiteUrl, string Section); +public sealed record RequestTypeDto(int Id, string Name, string PageUrl, string? ListId, string? SiteUrl, string Section); -public sealed record CreateRequestTypeRequest(string Name, string PageUrl, string? ListName, string? SiteUrl, string Section); +public sealed record CreateRequestTypeRequest(string Name, string PageUrl, string? ListId, string? SiteUrl, string Section); -public sealed record UpdateRequestTypeRequest(string Name, string PageUrl, string? ListName, string? SiteUrl, string Section); +public sealed record UpdateRequestTypeRequest(string Name, string PageUrl, string? ListId, string? SiteUrl, string Section); -public sealed record PatchRequestTypeRequest(string? Name, string? PageUrl, string? ListName, string? SiteUrl, string? Section); +public sealed record PatchRequestTypeRequest(string? Name, string? PageUrl, string? ListId, string? SiteUrl, string? Section); diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs index 83aface..296f600 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeService.cs @@ -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.SiteUrl, x.Section)) + .Select(x => new RequestTypeDto(x.Id, x.Name, x.PageUrl, x.ListId, x.SiteUrl, x.Section)) .ToListAsync(cancellationToken); return Result.Ok>(items); @@ -29,15 +29,15 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) public async Task> CreateAsync(CreateRequestTypeRequest request, CancellationToken cancellationToken) { - if (!RequestTypeFields.IsListReferenceValid(request.ListName, request.SiteUrl)) + if (!RequestTypeFields.IsListReferenceValid(request.ListId, request.SiteUrl)) return Result.Fail(new ValidationError( - "Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе.")); + "Id списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе.")); var entity = new RequestType { Name = request.Name, PageUrl = request.PageUrl, - ListName = request.ListName, + ListId = request.ListId, SiteUrl = request.SiteUrl, Section = request.Section }; @@ -53,13 +53,13 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) if (entity is null) return Result.Fail(new NotFoundError("Тип заявки", id)); - if (!RequestTypeFields.IsListReferenceValid(request.ListName, request.SiteUrl)) + if (!RequestTypeFields.IsListReferenceValid(request.ListId, request.SiteUrl)) return Result.Fail(new ValidationError( - "Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе.")); + "Id списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе.")); entity.Name = request.Name; entity.PageUrl = request.PageUrl; - entity.ListName = request.ListName; + entity.ListId = request.ListId; entity.SiteUrl = request.SiteUrl; entity.Section = request.Section; await db.SaveChangesAsync(cancellationToken); @@ -73,16 +73,16 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db) if (entity is null) return Result.Fail(new NotFoundError("Тип заявки", id)); - var listName = request.ListName ?? entity.ListName; + var listId = request.ListId ?? entity.ListId; var siteUrl = request.SiteUrl ?? entity.SiteUrl; - if (!RequestTypeFields.IsListReferenceValid(listName, siteUrl)) + if (!RequestTypeFields.IsListReferenceValid(listId, siteUrl)) return Result.Fail(new ValidationError( - "Имя списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе.")); + "Id списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе.")); entity.Name = request.Name ?? entity.Name; entity.PageUrl = request.PageUrl ?? entity.PageUrl; - entity.ListName = listName; + entity.ListId = listId; entity.SiteUrl = siteUrl; entity.Section = request.Section ?? entity.Section; await db.SaveChangesAsync(cancellationToken); @@ -102,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.SiteUrl, e.Section); + private static RequestTypeDto ToDto(RequestType e) => new(e.Id, e.Name, e.PageUrl, e.ListId, e.SiteUrl, e.Section); } diff --git a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs index 33852e9..8a47466 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/RequestTypes/RequestTypeValidators.cs @@ -10,8 +10,8 @@ public sealed class CreateRequestTypeRequestValidator : AbstractValidator 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 должны быть заполнены оба вместе или не заполнены вовсе."); + .Must(x => RequestTypeFields.IsListReferenceValid(x.ListId, x.SiteUrl)) + .WithMessage("Id списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе."); } } @@ -23,8 +23,8 @@ public sealed class UpdateRequestTypeRequestValidator : AbstractValidator 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 должны быть заполнены оба вместе или не заполнены вовсе."); + .Must(x => RequestTypeFields.IsListReferenceValid(x.ListId, x.SiteUrl)) + .WithMessage("Id списка и url сайта SharePoint должны быть заполнены оба вместе или не заполнены вовсе."); } } @@ -43,6 +43,6 @@ public sealed class PatchRequestTypeRequestValidator : AbstractValidator - string.IsNullOrWhiteSpace(listName) == string.IsNullOrWhiteSpace(siteUrl); + public static bool IsListReferenceValid(string? listId, string? siteUrl) => + string.IsNullOrWhiteSpace(listId) == string.IsNullOrWhiteSpace(siteUrl); } diff --git a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs index 4525edf..034ecc5 100644 --- a/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs +++ b/backend/src/Tnsu.ApprovalRoute.Application/TemplateRouteSteps/TemplateRouteStepService.cs @@ -46,9 +46,9 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) return Result.Fail(new ValidationError( "Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе.")); - if (RequiresListName(r.FieldName, r.Operation, r.Value) && string.IsNullOrWhiteSpace(route.RequestType?.ListName)) + if (RequiresList(r.FieldName, r.Operation, r.Value) && string.IsNullOrWhiteSpace(route.RequestType?.ListId)) return Result.Fail(new ValidationError( - "Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано имя списка (list_name).")); + "Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id).")); var duplicate = await HasDuplicateAsync( route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status, excludeId: null, cancellationToken); @@ -95,9 +95,9 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) return Result.Fail(new ValidationError( "Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе.")); - if (RequiresListName(r.FieldName, r.Operation, r.Value) && string.IsNullOrWhiteSpace(route.RequestType?.ListName)) + if (RequiresList(r.FieldName, r.Operation, r.Value) && string.IsNullOrWhiteSpace(route.RequestType?.ListId)) return Result.Fail(new ValidationError( - "Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано имя списка (list_name).")); + "Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id).")); var duplicate = await HasDuplicateAsync( route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status, excludeId: entity.Id, cancellationToken); @@ -155,9 +155,9 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) return Result.Fail(new ValidationError( "Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе.")); - if (RequiresListName(fieldName, operation, value) && string.IsNullOrWhiteSpace(route.RequestType?.ListName)) + if (RequiresList(fieldName, operation, value) && string.IsNullOrWhiteSpace(route.RequestType?.ListId)) return Result.Fail(new ValidationError( - "Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано имя списка (list_name).")); + "Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id).")); var duplicate = await HasDuplicateAsync( route.Id, executorEmail, position, projectRole, headType, status, excludeId: entity.Id, cancellationToken); @@ -192,7 +192,7 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db) return Result.Ok(); } - private static bool RequiresListName(string? fieldName, ComparisonOperator? operation, string? value) => + private static bool RequiresList(string? fieldName, ComparisonOperator? operation, string? value) => TemplateRouteStepFields.CountConditionFilled(fieldName, operation, value) > 0; private Task HasDuplicateAsync( diff --git a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs b/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs index 5b9f8bf..b47fa57 100644 --- a/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs +++ b/backend/src/Tnsu.ApprovalRoute.Domain/Entities/RequestType.cs @@ -5,7 +5,7 @@ public class RequestType public int Id { get; set; } public string Name { get; set; } = string.Empty; public string PageUrl { get; set; } = string.Empty; - public string? ListName { get; set; } + public string? ListId { get; set; } public string? SiteUrl { get; set; } public string Section { get; set; } = string.Empty; diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs index e9ba224..5aeb2f4 100644 --- a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/ApprovalRouteDbContext.cs @@ -16,7 +16,7 @@ public class ApprovalRouteDbContext(DbContextOptions opt e.HasKey(x => x.Id); 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.ListId).HasMaxLength(512); e.Property(x => x.SiteUrl).HasMaxLength(512); e.Property(x => x.Section).HasMaxLength(256).IsRequired(); }); diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.Designer.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.Designer.cs new file mode 100644 index 0000000..a248181 --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.Designer.cs @@ -0,0 +1,217 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Tnsu.ApprovalRoute.Infrastructure.Persistence; + +#nullable disable + +namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApprovalRouteDbContext))] + [Migration("20260803110943_RenameRequestTypeListNameToListId")] + partial class RenameRequestTypeListNameToListId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ListId") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("list_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("PageUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("page_url"); + + b.Property("Section") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("section"); + + b.Property("SiteUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("site_url"); + + b.HasKey("Id") + .HasName("pk_request_types"); + + b.ToTable("request_types", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ByProject") + .HasColumnType("boolean") + .HasColumnName("by_project"); + + b.Property("CompanyId") + .HasColumnType("uuid") + .HasColumnName("company_id"); + + b.Property("DepartmentId") + .HasColumnType("uuid") + .HasColumnName("department_id"); + + b.Property("RequestTypeId") + .HasColumnType("integer") + .HasColumnName("request_type_id"); + + b.Property("RequiresControl") + .HasColumnType("boolean") + .HasColumnName("requires_control"); + + b.HasKey("Id") + .HasName("pk_template_routes"); + + b.HasIndex("RequestTypeId", "DepartmentId", "CompanyId", "ByProject") + .IsUnique() + .HasDatabaseName("ix_template_routes_request_type_id_department_id_company_id_by"); + + b.ToTable("template_routes", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowReassignToOtherApprovers") + .HasColumnType("boolean") + .HasColumnName("allow_reassign_to_other_approvers"); + + b.Property("ExecutorEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("executor_email"); + + b.Property("FieldName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("field_name"); + + b.Property("HeadType") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("head_type"); + + b.Property("Operation") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("operation"); + + b.Property("OrderNo") + .HasColumnType("integer") + .HasColumnName("order_no"); + + b.Property("Position") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("position"); + + b.Property("ProjectRole") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("project_role"); + + b.Property("RouteId") + .HasColumnType("integer") + .HasColumnName("route_id"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("Value") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_template_route_steps"); + + b.HasIndex("RouteId", "OrderNo") + .HasDatabaseName("ix_template_route_steps_route_id_order_no"); + + b.ToTable("template_route_steps", (string)null); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.RequestType", "RequestType") + .WithMany("Routes") + .HasForeignKey("RequestTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_template_routes_request_types_request_type_id"); + + b.Navigation("RequestType"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRouteStep", b => + { + b.HasOne("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", "Route") + .WithMany("Steps") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_template_route_steps_template_routes_route_id"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.RequestType", b => + { + b.Navigation("Routes"); + }); + + modelBuilder.Entity("Tnsu.ApprovalRoute.Domain.Entities.TemplateRoute", b => + { + b.Navigation("Steps"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.cs new file mode 100644 index 0000000..5baa9fe --- /dev/null +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/20260803110943_RenameRequestTypeListNameToListId.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations +{ + /// + public partial class RenameRequestTypeListNameToListId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "list_name", + table: "request_types"); + + migrationBuilder.AddColumn( + name: "list_id", + table: "request_types", + type: "character varying(512)", + maxLength: 512, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "list_id", + table: "request_types"); + + migrationBuilder.AddColumn( + name: "list_name", + table: "request_types", + type: "character varying(256)", + maxLength: 256, + nullable: true); + } + } +} diff --git a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs index 09ed308..60b1783 100644 --- a/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs +++ b/backend/src/Tnsu.ApprovalRoute.Infrastructure/Persistence/Migrations/ApprovalRouteDbContextModelSnapshot.cs @@ -31,10 +31,10 @@ namespace Tnsu.ApprovalRoute.Infrastructure.Persistence.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("ListName") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("list_name"); + b.Property("ListId") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("list_id"); b.Property("Name") .IsRequired() diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs index d861696..c80ff05 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs @@ -74,7 +74,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I // Action var patched = await _service.PatchAsync( - created.Value.Id, new PatchRequestTypeRequest(Name: "Patched name", PageUrl: null, ListName: null, SiteUrl: null, Section: null), CancellationToken.None); + created.Value.Id, new PatchRequestTypeRequest(Name: "Patched name", PageUrl: null, ListId: 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, SiteUrl: null, Section: null), CancellationToken.None); + 999999, new PatchRequestTypeRequest(Name: "X", PageUrl: null, ListId: null, SiteUrl: null, Section: null), CancellationToken.None); // Assert result.IsFailed.ShouldBeTrue(); @@ -109,8 +109,8 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I fetched.IsFailed.ShouldBeTrue(); } - [Fact(DisplayName = "Создание типа заявки с list_name без site_url возвращает неуспешный результат")] - public async Task CreateAsync_ListNameWithoutSiteUrl_ReturnsFailedResult() + [Fact(DisplayName = "Создание типа заявки с list_id без site_url возвращает неуспешный результат")] + public async Task CreateAsync_ListIdWithoutSiteUrl_ReturnsFailedResult() { // Action var result = await _service.CreateAsync( @@ -120,8 +120,8 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I result.IsFailed.ShouldBeTrue(); } - [Fact(DisplayName = "Создание типа заявки с list_name и site_url успешно")] - public async Task CreateAsync_ListNameWithSiteUrl_ReturnsCreatedEntity() + [Fact(DisplayName = "Создание типа заявки с list_id и site_url успешно")] + public async Task CreateAsync_ListIdWithSiteUrl_ReturnsCreatedEntity() { // Action var created = await _service.CreateAsync( @@ -131,7 +131,7 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I // Assert created.IsSuccess.ShouldBeTrue(); - created.Value.ListName.ShouldBe("Requests"); + created.Value.ListId.ShouldBe("Requests"); created.Value.SiteUrl.ShouldBe("https://tnsukz.sharepoint.com/sites/hrm"); } diff --git a/frontend/src/webparts/approvalAdmin/components/screens/RequestTypesScreen.tsx b/frontend/src/webparts/approvalAdmin/components/screens/RequestTypesScreen.tsx index cb8de43..e72fb83 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/RequestTypesScreen.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/RequestTypesScreen.tsx @@ -6,6 +6,7 @@ import { texts } from '../../texts'; import { useRequestTypes } from '../../hooks/useRequestTypes'; import { useSharePointLists } from '../../hooks/useSharePointLists'; import { useSharePointSiteTitle } from '../../hooks/useSharePointSiteTitle'; +import { useSharePointListTitle } from '../../hooks/useSharePointListTitle'; import { useAsyncSubmit } from '../../hooks/useAsyncSubmit'; import { useDeleteTarget } from '../../hooks/useDeleteTarget'; import { @@ -36,10 +37,10 @@ interface IFormState { pageUrl: string; section: string; siteUrl: string; - listName: string; + listId: string; } -const EMPTY_FORM: IFormState = { name: '', pageUrl: '', section: '', siteUrl: '', listName: '' }; +const EMPTY_FORM: IFormState = { name: '', pageUrl: '', section: '', siteUrl: '', listId: '' }; /** Ячейка «Сайт SharePoint» — подтягивает заголовок сайта по url, хранящемуся на бэкенде. */ const SiteUrlCell: React.FC<{ client: SharePointListsClient; siteUrl: string | null }> = ({ client, siteUrl }) => { @@ -47,6 +48,12 @@ const SiteUrlCell: React.FC<{ client: SharePointListsClient; siteUrl: string | n return <>{siteUrl ? formatSiteLabel(title, siteUrl) : texts.common.empty}; }; +/** Ячейка «Список SharePoint» — на бэкенде хранится id списка (устойчив к переименованию), тут подтягиваем Title для читаемости. */ +const ListIdCell: React.FC<{ client: SharePointListsClient; siteUrl: string | null; listId: string | null }> = ({ client, siteUrl, listId }) => { + const { title } = useSharePointListTitle(client, siteUrl, listId); + return <>{listId ? (title ?? listId) : texts.common.empty}; +}; + export const RequestTypesScreen: React.FC = ({ api, sharePoint }) => { const list = useRequestTypes(api); @@ -76,7 +83,7 @@ export const RequestTypesScreen: React.FC = ({ api, sh pageUrl: item.pageUrl, section: item.section, siteUrl: item.siteUrl ?? '', - listName: item.listName ?? '' + listId: item.listId ?? '' }); setShowForm(true); }; @@ -87,8 +94,8 @@ export const RequestTypesScreen: React.FC = ({ api, sh return; } const siteUrl = form.siteUrl.trim(); - const listName = form.listName.trim(); - if (Boolean(siteUrl) !== Boolean(listName)) { + const listId = form.listId.trim(); + if (Boolean(siteUrl) !== Boolean(listId)) { submit.fail(texts.requestTypes.validationListReference); return; } @@ -98,7 +105,7 @@ export const RequestTypesScreen: React.FC = ({ api, sh pageUrl: form.pageUrl.trim(), section: form.section.trim(), siteUrl: siteUrl || null, - listName: listName || null + listId: listId || null }; const request = editing ? api.requestTypes.update(editing.id, input) : api.requestTypes.create(input); @@ -110,7 +117,8 @@ export const RequestTypesScreen: React.FC = ({ api, sh ); }; - const listNameOptions = sharePointLists.items.map(l => ({ value: l.title, label: l.title })); + // value — server-relative url списка (устойчив к переименованию Title в SharePoint), label — Title для читаемости. + const listIdOptions = sharePointLists.items.map(l => ({ value: l.url, label: l.title })); return (
@@ -130,7 +138,7 @@ export const RequestTypesScreen: React.FC = ({ api, sh { key: 'name', header: texts.requestTypes.columnName, render: r => {r.name} }, { key: 'url', header: texts.requestTypes.columnPageUrl, render: r => r.pageUrl }, { key: 'section', header: texts.requestTypes.columnSection, render: r => r.section }, - { key: 'listName', header: texts.requestTypes.columnListName, render: r => r.listName ?? texts.common.empty }, + { key: 'listId', header: texts.requestTypes.columnListId, render: r => }, { key: 'siteUrl', header: texts.requestTypes.columnSiteUrl, render: r => }, { key: 'actions', @@ -196,29 +204,29 @@ export const RequestTypesScreen: React.FC = ({ api, sh getSublabel={s => s.url} onChange={(siteUrl, item) => { if (siteUrl && item) sharePoint.cacheSiteTitle(siteUrl, item.title); - setForm({ ...form, siteUrl, listName: '' }); + setForm({ ...form, siteUrl, listId: '' }); }} />