Несколько условий шага, объединяемых через И/ИЛИ (#16)
<html> <body> <!--StartFragment--> GetEmployeeByEmailAsync возвращает массив; новый HeadType.CompanyDirectors -- <!--EndFragment--> </body> </html>[GetEmployeeByEmailAsync возвращает массив; новый HeadType.CompanyDirectors](http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/commit/572749b4d7cc11bc8e1ef4184a93f18a14b710ab) --------- Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/16
This commit was merged in pull request #16.
This commit is contained in:
+127
-84
@@ -15,14 +15,12 @@ public sealed class TemplateRouteStepService(
|
||||
public async Task<Result<IReadOnlyList<TemplateRouteStepDto>>> ListAsync(int routeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var items = await db.TemplateRouteSteps.AsNoTracking()
|
||||
.Include(x => x.Conditions)
|
||||
.Where(x => x.RouteId == routeId)
|
||||
.OrderBy(x => x.OrderNo)
|
||||
.Select(x => new TemplateRouteStepDto(
|
||||
x.Id, x.OrderNo, x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType, x.Status, x.RouteId, x.AllowReassignToOtherApprovers,
|
||||
x.FieldName, x.Operation, x.Value, null, x.SlaHours, x.AskOverdueReason))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Ok<IReadOnlyList<TemplateRouteStepDto>>(items);
|
||||
return Result.Ok<IReadOnlyList<TemplateRouteStepDto>>(items.Select(ToDto).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -38,19 +36,18 @@ public sealed class TemplateRouteStepService(
|
||||
return Result.Ok<IReadOnlyList<TemplateRouteStepDto>>(Array.Empty<TemplateRouteStepDto>());
|
||||
|
||||
var items = await db.TemplateRouteSteps.AsNoTracking()
|
||||
.Include(x => x.Conditions)
|
||||
.Where(x => x.ExecutorEmail != null && EF.Functions.ILike(x.ExecutorEmail, executorEmail))
|
||||
.OrderBy(x => x.RouteId).ThenBy(x => x.OrderNo)
|
||||
.Select(x => new TemplateRouteStepDto(
|
||||
x.Id, x.OrderNo, x.ExecutorEmail, x.Position, x.ProjectRole, x.HeadType, x.Status, x.RouteId, x.AllowReassignToOtherApprovers,
|
||||
x.FieldName, x.Operation, x.Value, null, x.SlaHours, x.AskOverdueReason))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Ok<IReadOnlyList<TemplateRouteStepDto>>(items);
|
||||
return Result.Ok<IReadOnlyList<TemplateRouteStepDto>>(items.Select(ToDto).ToList());
|
||||
}
|
||||
|
||||
public async Task<Result<TemplateRouteStepDto>> GetAsync(int routeId, int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.TemplateRouteSteps.AsNoTracking()
|
||||
.Include(x => x.Conditions)
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
|
||||
|
||||
return entity is null
|
||||
@@ -68,22 +65,22 @@ public sealed class TemplateRouteStepService(
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
|
||||
|
||||
if (!TemplateRouteStepFields.IsConditionValid(r.FieldName, r.Operation, r.Value))
|
||||
if (!TemplateRouteStepFields.AreConditionsValid(r.Conditions))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе."));
|
||||
"В каждом условии (field_name, operation, value) все три поля обязательны."));
|
||||
|
||||
if (RequiresList(r.FieldName, r.Operation, r.Value) && string.IsNullOrWhiteSpace(route.RequestType?.ListId))
|
||||
if (RequiresList(r.Conditions) && string.IsNullOrWhiteSpace(route.RequestType?.ListId))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id)."));
|
||||
"Условия появления шага можно указывать только если у типа заявки указано id списка (list_id)."));
|
||||
|
||||
var duplicate = await HasDuplicateAsync(
|
||||
route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status,
|
||||
r.FieldName, r.Operation, r.Value, excludeId: null, cancellationToken);
|
||||
r.Conditions, r.ConditionsLogic, excludeId: null, cancellationToken);
|
||||
if (duplicate)
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя, статусом и условием появления уже существует в этом маршруте."));
|
||||
|
||||
if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, r.FieldName, r.Operation, r.Value, excludeId: null, cancellationToken))
|
||||
if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, r.Conditions, r.ConditionsLogic, excludeId: null, cancellationToken))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
$"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» и таким же условием появления — второй такой шаг недопустим."));
|
||||
|
||||
@@ -97,9 +94,8 @@ public sealed class TemplateRouteStepService(
|
||||
HeadType = r.HeadType,
|
||||
Status = r.Status,
|
||||
AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers,
|
||||
FieldName = r.FieldName,
|
||||
Operation = r.Operation,
|
||||
Value = r.Value,
|
||||
ConditionsLogic = r.ConditionsLogic,
|
||||
Conditions = ToEntities(r.Conditions),
|
||||
SlaHours = r.SlaHours,
|
||||
AskOverdueReason = r.AskOverdueReason
|
||||
};
|
||||
@@ -118,7 +114,8 @@ public sealed class TemplateRouteStepService(
|
||||
if (route is null)
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
|
||||
|
||||
var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
|
||||
var entity = await db.TemplateRouteSteps.Include(x => x.Conditions)
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", id));
|
||||
|
||||
@@ -126,22 +123,22 @@ public sealed class TemplateRouteStepService(
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
|
||||
|
||||
if (!TemplateRouteStepFields.IsConditionValid(r.FieldName, r.Operation, r.Value))
|
||||
if (!TemplateRouteStepFields.AreConditionsValid(r.Conditions))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе."));
|
||||
"В каждом условии (field_name, operation, value) все три поля обязательны."));
|
||||
|
||||
if (RequiresList(r.FieldName, r.Operation, r.Value) && string.IsNullOrWhiteSpace(route.RequestType?.ListId))
|
||||
if (RequiresList(r.Conditions) && string.IsNullOrWhiteSpace(route.RequestType?.ListId))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id)."));
|
||||
"Условия появления шага можно указывать только если у типа заявки указано id списка (list_id)."));
|
||||
|
||||
var duplicate = await HasDuplicateAsync(
|
||||
route.Id, r.ExecutorEmail, r.Position, r.ProjectRole, r.HeadType, r.Status,
|
||||
r.FieldName, r.Operation, r.Value, excludeId: entity.Id, cancellationToken);
|
||||
r.Conditions, r.ConditionsLogic, excludeId: entity.Id, cancellationToken);
|
||||
if (duplicate)
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя, статусом и условием появления уже существует в этом маршруте."));
|
||||
|
||||
if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, r.FieldName, r.Operation, r.Value, excludeId: entity.Id, cancellationToken))
|
||||
if (await HasDuplicateHeadTypeAsync(route.Id, r.HeadType, r.Conditions, r.ConditionsLogic, excludeId: entity.Id, cancellationToken))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
$"В маршруте уже есть шаг с типом руководителя «{r.HeadType}» и таким же условием появления — второй такой шаг недопустим."));
|
||||
|
||||
@@ -152,9 +149,8 @@ public sealed class TemplateRouteStepService(
|
||||
entity.HeadType = r.HeadType;
|
||||
entity.Status = r.Status;
|
||||
entity.AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers;
|
||||
entity.FieldName = r.FieldName;
|
||||
entity.Operation = r.Operation;
|
||||
entity.Value = r.Value;
|
||||
entity.ConditionsLogic = r.ConditionsLogic;
|
||||
ReplaceConditions(entity, r.Conditions);
|
||||
entity.SlaHours = r.SlaHours;
|
||||
entity.AskOverdueReason = r.AskOverdueReason;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
@@ -171,7 +167,8 @@ public sealed class TemplateRouteStepService(
|
||||
if (route is null)
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
|
||||
|
||||
var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
|
||||
var entity = await db.TemplateRouteSteps.Include(x => x.Conditions)
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", id));
|
||||
|
||||
@@ -180,9 +177,8 @@ public sealed class TemplateRouteStepService(
|
||||
var projectRole = r.ProjectRole ?? entity.ProjectRole;
|
||||
var headType = r.HeadType ?? entity.HeadType;
|
||||
var status = r.Status ?? entity.Status;
|
||||
var fieldName = r.FieldName ?? entity.FieldName;
|
||||
var operation = r.Operation ?? entity.Operation;
|
||||
var value = r.Value ?? entity.Value;
|
||||
var conditions = r.Conditions ?? ToDtos(entity.Conditions);
|
||||
var conditionsLogic = r.ConditionsLogic ?? entity.ConditionsLogic;
|
||||
|
||||
if (status == RouteStepStatus.None)
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError("Статус обязателен: Approval или Execution."));
|
||||
@@ -195,22 +191,22 @@ public sealed class TemplateRouteStepService(
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Должно быть заполнено ровно одно из полей: исполнитель, должность, роль в проекте или тип руководителя."));
|
||||
|
||||
if (!TemplateRouteStepFields.IsConditionValid(fieldName, operation, value))
|
||||
if (!TemplateRouteStepFields.AreConditionsValid(conditions))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Поля условия (field_name, operation, value) должны быть заполнены все вместе или не заполнены вовсе."));
|
||||
"В каждом условии (field_name, operation, value) все три поля обязательны."));
|
||||
|
||||
if (RequiresList(fieldName, operation, value) && string.IsNullOrWhiteSpace(route.RequestType?.ListId))
|
||||
if (RequiresList(conditions) && string.IsNullOrWhiteSpace(route.RequestType?.ListId))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Поля условия (field_name, operation, value) можно указывать только если у типа заявки указано id списка (list_id)."));
|
||||
"Условия появления шага можно указывать только если у типа заявки указано id списка (list_id)."));
|
||||
|
||||
var duplicate = await HasDuplicateAsync(
|
||||
route.Id, executorEmail, position, projectRole, headType, status,
|
||||
fieldName, operation, value, excludeId: entity.Id, cancellationToken);
|
||||
conditions, conditionsLogic, excludeId: entity.Id, cancellationToken);
|
||||
if (duplicate)
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя, статусом и условием появления уже существует в этом маршруте."));
|
||||
|
||||
if (await HasDuplicateHeadTypeAsync(route.Id, headType, fieldName, operation, value, excludeId: entity.Id, cancellationToken))
|
||||
if (await HasDuplicateHeadTypeAsync(route.Id, headType, conditions, conditionsLogic, excludeId: entity.Id, cancellationToken))
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
$"В маршруте уже есть шаг с типом руководителя «{headType}» и таким же условием появления — второй такой шаг недопустим."));
|
||||
|
||||
@@ -221,9 +217,9 @@ public sealed class TemplateRouteStepService(
|
||||
entity.HeadType = headType;
|
||||
entity.Status = status;
|
||||
entity.AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers ?? entity.AllowReassignToOtherApprovers;
|
||||
entity.FieldName = fieldName;
|
||||
entity.Operation = operation;
|
||||
entity.Value = value;
|
||||
entity.ConditionsLogic = conditionsLogic;
|
||||
if (r.Conditions is not null)
|
||||
ReplaceConditions(entity, r.Conditions);
|
||||
// Как и остальные поля в PATCH, null значит «не менять». Вернуть шаг к
|
||||
// наследованию SLA от типа заявки можно через PUT.
|
||||
entity.SlaHours = r.SlaHours ?? entity.SlaHours;
|
||||
@@ -249,61 +245,108 @@ public sealed class TemplateRouteStepService(
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
private static bool RequiresList(string? fieldName, ComparisonOperator? operation, string? value) =>
|
||||
TemplateRouteStepFields.CountConditionFilled(fieldName, operation, value) > 0;
|
||||
private static bool RequiresList(IReadOnlyList<RouteStepConditionDto>? conditions) => conditions is { Count: > 0 };
|
||||
|
||||
private static List<TemplateRouteStepCondition> ToEntities(IReadOnlyList<RouteStepConditionDto>? conditions) =>
|
||||
(conditions ?? []).Select((c, i) => new TemplateRouteStepCondition
|
||||
{
|
||||
OrderNo = i,
|
||||
FieldName = c.FieldName,
|
||||
Operation = c.Operation,
|
||||
Value = c.Value
|
||||
}).ToList();
|
||||
|
||||
private static void ReplaceConditions(TemplateRouteStep entity, IReadOnlyList<RouteStepConditionDto>? conditions)
|
||||
{
|
||||
entity.Conditions.Clear();
|
||||
entity.Conditions.AddRange(ToEntities(conditions));
|
||||
}
|
||||
|
||||
private static List<RouteStepConditionDto> ToDtos(IEnumerable<TemplateRouteStepCondition> conditions) =>
|
||||
conditions.OrderBy(c => c.OrderNo).Select(c => new RouteStepConditionDto(c.FieldName, c.Operation, c.Value)).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Шаги с одинаковым исполнителем считаются дубликатами только при одинаковом
|
||||
/// условии появления (field_name/operation/value): шаги с разными условиями
|
||||
/// взаимоисключающи по данным заявки, поэтому в один маршрут они попасть могут,
|
||||
/// а в один резолв — нет.
|
||||
/// условии появления (набор условий + логика их объединения): шаги с разными
|
||||
/// условиями взаимоисключающи по данным заявки, поэтому в один маршрут они
|
||||
/// попасть могут, а в один резолв — нет. Сравнение условий — в памяти
|
||||
/// (см. <see cref="ConditionsEqual"/>): кандидатов на дубликат в пределах
|
||||
/// одного маршрута всегда мало, а сравнение списка условий целиком плохо
|
||||
/// транслируется в SQL.
|
||||
/// </summary>
|
||||
private Task<bool> HasDuplicateAsync(
|
||||
private async Task<bool> HasDuplicateAsync(
|
||||
int routeId, string? executorEmail, string? position, ProjectRole? projectRole, HeadType? headType,
|
||||
RouteStepStatus status, string? fieldName, ComparisonOperator? operation, string? value,
|
||||
int? excludeId, CancellationToken cancellationToken) =>
|
||||
db.TemplateRouteSteps.AnyAsync(x =>
|
||||
x.Id != excludeId &&
|
||||
x.RouteId == routeId &&
|
||||
x.ExecutorEmail == executorEmail &&
|
||||
x.Position == position &&
|
||||
x.ProjectRole == projectRole &&
|
||||
x. HeadType == headType &&
|
||||
x.Status == status &&
|
||||
x.FieldName == fieldName &&
|
||||
x.Operation == operation &&
|
||||
x.Value == value,
|
||||
cancellationToken);
|
||||
RouteStepStatus status, IReadOnlyList<RouteStepConditionDto>? conditions, ConditionsLogic conditionsLogic,
|
||||
int? excludeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var candidates = await db.TemplateRouteSteps.AsNoTracking().Include(x => x.Conditions)
|
||||
.Where(x =>
|
||||
x.Id != excludeId &&
|
||||
x.RouteId == routeId &&
|
||||
x.ExecutorEmail == executorEmail &&
|
||||
x.Position == position &&
|
||||
x.ProjectRole == projectRole &&
|
||||
x.HeadType == headType &&
|
||||
x.Status == status)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return candidates.Any(x => ConditionsEqual(ToDtos(x.Conditions), x.ConditionsLogic, conditions, conditionsLogic));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Любой HeadType (Chief/DirectionHead/DirectorTansu/DepartmentHead) резолвится
|
||||
/// в ОДНОГО конкретного человека — цепочка подчинённости инициатора или, для
|
||||
/// DepartmentHead, единственный departmentId запроса (см.
|
||||
/// ResolveTemplateRouteRequest.DepartmentId) не оставляют места для выбора.
|
||||
/// Поэтому два шага одного маршрута с одинаковым HeadType всегда резолвились бы
|
||||
/// в одного и того же (или неотличимо какого) руководителя — например, «два
|
||||
/// разных директора одной и той же компании» просто не существуют. В отличие
|
||||
/// от HasDuplicateAsync (точное совпадение ВСЕХ полей включая статус), здесь
|
||||
/// достаточно совпадения HeadType — вне зависимости от статуса или остальных
|
||||
/// полей шага. Исключение — условие появления шага: при разных условиях
|
||||
/// в конкретный резолв попадёт только один из шагов, так что одного и того же
|
||||
/// руководителя дважды в маршруте не окажется.
|
||||
/// Любой HeadType (Chief/DirectionHead/DirectorTansu/DepartmentHead/CompanyDirectors)
|
||||
/// резолвится в один и тот же ДЕТЕРМИНИРОВАННЫЙ результат — цепочка подчинённости
|
||||
/// инициатора, единственный departmentId запроса (для DepartmentHead, см.
|
||||
/// ResolveTemplateRouteRequest.DepartmentId) или компании самого инициатора (для
|
||||
/// CompanyDirectors) не оставляют места для выбора. Для большинства значений это
|
||||
/// один конкретный человек; для CompanyDirectors — один и тот же набор людей
|
||||
/// (директор каждой компании инициатора), но тоже всегда один и тот же для
|
||||
/// одного и того же инициатора. Поэтому два шага одного маршрута с одинаковым
|
||||
/// HeadType всегда резолвились бы одинаково — например, «два разных директора
|
||||
/// одной и той же компании» просто не существуют. В отличие от HasDuplicateAsync
|
||||
/// (точное совпадение ВСЕХ полей включая статус), здесь достаточно совпадения
|
||||
/// HeadType — вне зависимости от статуса или остальных полей шага. Исключение —
|
||||
/// условие появления шага: при разных условиях в конкретный резолв попадёт только
|
||||
/// один из шагов, так что дублирования исполнителей в маршруте не окажется.
|
||||
/// </summary>
|
||||
private Task<bool> HasDuplicateHeadTypeAsync(
|
||||
int routeId, HeadType? headType, string? fieldName, ComparisonOperator? operation, string? value,
|
||||
private async Task<bool> HasDuplicateHeadTypeAsync(
|
||||
int routeId, HeadType? headType, IReadOnlyList<RouteStepConditionDto>? conditions, ConditionsLogic conditionsLogic,
|
||||
int? excludeId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (headType is null or HeadType.None)
|
||||
return Task.FromResult(false);
|
||||
return false;
|
||||
|
||||
return db.TemplateRouteSteps.AnyAsync(x =>
|
||||
x.Id != excludeId &&
|
||||
x.RouteId == routeId &&
|
||||
x.HeadType == headType &&
|
||||
x.FieldName == fieldName &&
|
||||
x.Operation == operation &&
|
||||
x.Value == value,
|
||||
cancellationToken);
|
||||
var candidates = await db.TemplateRouteSteps.AsNoTracking().Include(x => x.Conditions)
|
||||
.Where(x => x.Id != excludeId && x.RouteId == routeId && x.HeadType == headType)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return candidates.Any(x => ConditionsEqual(ToDtos(x.Conditions), x.ConditionsLogic, conditions, conditionsLogic));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Наборы условий равны, если совпадают все условия (без учёта порядка — порядок
|
||||
/// влияет только на отображение в форме, не на вычисление) и, если условий два и
|
||||
/// более, логика их объединения (И/ИЛИ). При 0-1 условии логика ни на что не
|
||||
/// влияет, поэтому не сравнивается.
|
||||
/// </summary>
|
||||
private static bool ConditionsEqual(
|
||||
IReadOnlyList<RouteStepConditionDto> a, ConditionsLogic aLogic,
|
||||
IReadOnlyList<RouteStepConditionDto>? b, ConditionsLogic bLogic)
|
||||
{
|
||||
var bList = b ?? [];
|
||||
if (a.Count != bList.Count)
|
||||
return false;
|
||||
if (a.Count > 1 && aLogic != bLogic)
|
||||
return false;
|
||||
|
||||
static IEnumerable<(string FieldName, ComparisonOperator Operation, string Value)> Normalize(
|
||||
IReadOnlyList<RouteStepConditionDto> c) =>
|
||||
c.Select(x => (x.FieldName, x.Operation, x.Value))
|
||||
.OrderBy(x => x.FieldName, StringComparer.Ordinal)
|
||||
.ThenBy(x => x.Operation)
|
||||
.ThenBy(x => x.Value, StringComparer.Ordinal);
|
||||
|
||||
return Normalize(a).SequenceEqual(Normalize(bList));
|
||||
}
|
||||
|
||||
private async Task<TemplateRouteStepDto> ToDtoWithWarningAsync(
|
||||
@@ -323,5 +366,5 @@ public sealed class TemplateRouteStepService(
|
||||
|
||||
private static TemplateRouteStepDto ToDto(TemplateRouteStep e) => new(
|
||||
e.Id, e.OrderNo, e.ExecutorEmail, e.Position, e.ProjectRole, e.HeadType, e.Status, e.RouteId, e.AllowReassignToOtherApprovers,
|
||||
e.FieldName, e.Operation, e.Value, null, e.SlaHours, e.AskOverdueReason);
|
||||
ToDtos(e.Conditions), e.ConditionsLogic, null, e.SlaHours, e.AskOverdueReason);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user