Отдавать в /api/request-type-access список SharePoint-списков типа
Доступ здесь настраивает только видимость в UI approval-route — права SharePoint остаются ручным шагом, и раньше администратору приходилось отдельно смотреть, в какие списки данных (архив + текущая партиция) заводить чтение для email. Теперь RequestTypeAccessDto несёт готовую серию (переиспользует ListPartitionService), и админка показывает её колонкой в таблице. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,18 @@
|
|||||||
|
using Tnsu.ApprovalRoute.Application.ListPartitioning;
|
||||||
|
|
||||||
namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses;
|
namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses;
|
||||||
|
|
||||||
public sealed record RequestTypeAccessDto(int Id, int RequestTypeId, string Email);
|
/// <param name="SharePointLists">
|
||||||
|
/// Вся серия SharePoint-списков данных (архив + текущая партиция) типа заявки —
|
||||||
|
/// именно на них нужно выдать email'у право чтения, иначе поиск по порталу
|
||||||
|
/// продолжит отдавать только его собственные заявки: доступ здесь настраивает
|
||||||
|
/// видимость в UI approval-route, но не права SharePoint, которые остаются
|
||||||
|
/// отдельным ручным шагом.
|
||||||
|
/// </param>
|
||||||
|
public sealed record RequestTypeAccessDto(
|
||||||
|
int Id,
|
||||||
|
int RequestTypeId,
|
||||||
|
string Email,
|
||||||
|
IReadOnlyList<RequestListPartitionDto> SharePointLists);
|
||||||
|
|
||||||
public sealed record CreateRequestTypeAccessRequest(int RequestTypeId, string Email);
|
public sealed record CreateRequestTypeAccessRequest(int RequestTypeId, string Email);
|
||||||
|
|||||||
+33
-4
@@ -3,26 +3,53 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
||||||
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
||||||
|
using Tnsu.ApprovalRoute.Application.ListPartitioning;
|
||||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
using Tnsu.ApprovalRoute.Domain.Entities;
|
||||||
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||||
|
|
||||||
namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses;
|
namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses;
|
||||||
|
|
||||||
public sealed class RequestTypeAccessService(
|
public sealed class RequestTypeAccessService(
|
||||||
ApprovalRouteDbContext db, ICurrentUser currentUser, ILogger<RequestTypeAccessService> logger)
|
ApprovalRouteDbContext db,
|
||||||
|
ListPartitionService partitions,
|
||||||
|
ICurrentUser currentUser,
|
||||||
|
ILogger<RequestTypeAccessService> logger)
|
||||||
{
|
{
|
||||||
public async Task<Result<IReadOnlyList<RequestTypeAccessDto>>> ListAsync(
|
public async Task<Result<IReadOnlyList<RequestTypeAccessDto>>> ListAsync(
|
||||||
int? requestTypeId, CancellationToken cancellationToken)
|
int? requestTypeId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var items = await db.RequestTypeAccesses.AsNoTracking()
|
var entities = await db.RequestTypeAccesses.AsNoTracking()
|
||||||
.Where(x => requestTypeId == null || x.RequestTypeId == requestTypeId)
|
.Where(x => requestTypeId == null || x.RequestTypeId == requestTypeId)
|
||||||
.OrderBy(x => x.RequestTypeId).ThenBy(x => x.Email)
|
.OrderBy(x => x.RequestTypeId).ThenBy(x => x.Email)
|
||||||
.Select(x => new RequestTypeAccessDto(x.Id, x.RequestTypeId, x.Email))
|
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Партиции запрашиваются РАЗ на тип заявки, а не на строку — иначе список
|
||||||
|
// из N email по одному типу заново обходил бы SharePoint N раз подряд.
|
||||||
|
var listsByType = new Dictionary<int, IReadOnlyList<RequestListPartitionDto>>();
|
||||||
|
var items = new List<RequestTypeAccessDto>(entities.Count);
|
||||||
|
foreach (var e in entities)
|
||||||
|
{
|
||||||
|
var lists = await GetListsCachedAsync(listsByType, e.RequestTypeId, cancellationToken);
|
||||||
|
items.Add(new RequestTypeAccessDto(e.Id, e.RequestTypeId, e.Email, lists));
|
||||||
|
}
|
||||||
|
|
||||||
return Result.Ok<IReadOnlyList<RequestTypeAccessDto>>(items);
|
return Result.Ok<IReadOnlyList<RequestTypeAccessDto>>(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<RequestListPartitionDto>> GetListsCachedAsync(
|
||||||
|
Dictionary<int, IReadOnlyList<RequestListPartitionDto>> cache,
|
||||||
|
int requestTypeId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (cache.TryGetValue(requestTypeId, out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var result = await partitions.ListAsync(requestTypeId, cancellationToken);
|
||||||
|
var lists = result.IsSuccess ? result.Value : Array.Empty<RequestListPartitionDto>();
|
||||||
|
cache[requestTypeId] = lists;
|
||||||
|
return lists;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Список email'ов с доступом к типу заявок — дёргает Power Automate.</summary>
|
/// <summary>Список email'ов с доступом к типу заявок — дёргает Power Automate.</summary>
|
||||||
public async Task<Result<IReadOnlyList<string>>> ListEmailsAsync(int requestTypeId, CancellationToken cancellationToken)
|
public async Task<Result<IReadOnlyList<string>>> ListEmailsAsync(int requestTypeId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -58,7 +85,9 @@ public sealed class RequestTypeAccessService(
|
|||||||
"Доступ к типу заявки {RequestTypeId} выдан {Email} пользователем {Author}",
|
"Доступ к типу заявки {RequestTypeId} выдан {Email} пользователем {Author}",
|
||||||
entity.RequestTypeId, entity.Email, currentUser.Email);
|
entity.RequestTypeId, entity.Email, currentUser.Email);
|
||||||
|
|
||||||
return Result.Ok(new RequestTypeAccessDto(entity.Id, entity.RequestTypeId, entity.Email));
|
var listsResult = await partitions.ListAsync(entity.RequestTypeId, cancellationToken);
|
||||||
|
var lists = listsResult.IsSuccess ? listsResult.Value : Array.Empty<RequestListPartitionDto>();
|
||||||
|
return Result.Ok(new RequestTypeAccessDto(entity.Id, entity.RequestTypeId, entity.Email, lists));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> DeleteAsync(int id, CancellationToken cancellationToken)
|
public async Task<Result> DeleteAsync(int id, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
|||||||
|
|
||||||
const requestTypeName = (id: number): string => requestTypes.items.find(rt => rt.id === id)?.name ?? `#${id}`;
|
const requestTypeName = (id: number): string => requestTypes.items.find(rt => rt.id === id)?.name ?? `#${id}`;
|
||||||
|
|
||||||
|
// Список SharePoint нужно вручную открыть на чтение этому email — иначе доступ
|
||||||
|
// здесь настраивает только видимость в UI approval-route, но не в самом
|
||||||
|
// SharePoint (поиск по порталу отфильтрует чужие заявки правами списка).
|
||||||
|
const formatSharePointLists = (item: RequestTypeAccess): string =>
|
||||||
|
item.sharePointLists.length === 0
|
||||||
|
? texts.common.empty
|
||||||
|
: item.sharePointLists.map(l => l.title ?? l.listId).join(', ');
|
||||||
|
|
||||||
const openCreate = (): void => {
|
const openCreate = (): void => {
|
||||||
setRequestTypeId('');
|
setRequestTypeId('');
|
||||||
setEmail('');
|
setEmail('');
|
||||||
@@ -86,6 +94,7 @@ export const RequestTypeAccessScreen: React.FC<IRequestTypeAccessScreenProps> =
|
|||||||
columns={[
|
columns={[
|
||||||
{ key: 'requestType', header: texts.requestTypeAccess.columnRequestType, render: r => <PrimaryCell>{requestTypeName(r.requestTypeId)}</PrimaryCell> },
|
{ key: 'requestType', header: texts.requestTypeAccess.columnRequestType, render: r => <PrimaryCell>{requestTypeName(r.requestTypeId)}</PrimaryCell> },
|
||||||
{ key: 'email', header: texts.requestTypeAccess.columnEmail, render: r => r.email },
|
{ key: 'email', header: texts.requestTypeAccess.columnEmail, render: r => r.email },
|
||||||
|
{ key: 'sharePointLists', header: texts.requestTypeAccess.columnSharePointLists, render: formatSharePointLists },
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: '',
|
header: '',
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
// DTO эндпоинта api/request-type-access (Application/RequestTypeAccesses/*.cs).
|
// DTO эндпоинта api/request-type-access (Application/RequestTypeAccesses/*.cs).
|
||||||
|
import { RequestListPartition } from './requestTypeLists';
|
||||||
|
|
||||||
export interface RequestTypeAccess {
|
export interface RequestTypeAccess {
|
||||||
id: number;
|
id: number;
|
||||||
requestTypeId: number;
|
requestTypeId: number;
|
||||||
email: string;
|
email: string;
|
||||||
|
/**
|
||||||
|
* Вся серия SharePoint-списков данных типа заявки (архив + текущая партиция) —
|
||||||
|
* именно на них нужно вручную выдать email'у право чтения, иначе поиск по
|
||||||
|
* порталу продолжит отдавать только его собственные заявки: этот реестр
|
||||||
|
* настраивает видимость в UI approval-route, а не права SharePoint.
|
||||||
|
*/
|
||||||
|
sharePointLists: RequestListPartition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateRequestTypeAccessInput {
|
export interface CreateRequestTypeAccessInput {
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ export const texts = {
|
|||||||
emptyList: 'Пока никому не выдан доступ ни к одному типу заявок.',
|
emptyList: 'Пока никому не выдан доступ ни к одному типу заявок.',
|
||||||
columnRequestType: 'Тип заявки',
|
columnRequestType: 'Тип заявки',
|
||||||
columnEmail: 'Сотрудник',
|
columnEmail: 'Сотрудник',
|
||||||
|
columnSharePointLists: 'Списки SharePoint (выдать доступ вручную)',
|
||||||
createTitle: 'Выдать доступ',
|
createTitle: 'Выдать доступ',
|
||||||
requestTypeLabel: 'Тип заявки',
|
requestTypeLabel: 'Тип заявки',
|
||||||
requestTypePlaceholder: 'Выберите тип заявки',
|
requestTypePlaceholder: 'Выберите тип заявки',
|
||||||
|
|||||||
Reference in New Issue
Block a user