From 8b09243c2e19c98d46ced00c0bf72654ba367ce6 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 21 Aug 2026 17:23:19 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D1=87=D0=B8=D0=BD=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=B0=D1=86=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B5?= =?UTF-8?q?=20=D1=81=D0=BE=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BE?= =?UTF-8?q?=D0=BC=20=C2=AB=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Регрессия из 0cf8359: initialSubmitStatus стала возвращать «Выполнение» для маршрута без согласующих, но writeRoute определяла факт отправки строгим сравнением `status === onApproval`. В результате для такой заявки: - строки маршрута писались со статусом «Выполнение» — недопустимым для строки значением (его нет в APPROVAL_ROW_STATUS_CHOICES) вместо «Ждёт очереди»; - activateNextQueued не вызывался вовсе, поэтому ни одна строка не получала «На исполнении». Очередь исполнителя фильтрует строки ровно по «На исполнении», поэтому отправленные заявки в неё не попадали. Заменено на предикат isSubmittedStatus, который покрывает все статусы, возвращаемые initialSubmitStatus. Тест связывает эти две функции напрямую, чтобы добавление нового статуса отправки не могло снова разойтись с writeRoute молча. Co-Authored-By: Claude Sonnet 5 --- .../approval/approvalSubmission.service.ts | 8 +++- .../requestPortal/shared/approval/index.ts | 1 + .../shared/approval/models/statuses.test.ts | 39 +++++++++++++++++++ .../shared/approval/models/statuses.ts | 21 ++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts b/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts index 3e38182..f09b1ab 100644 --- a/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts +++ b/src/webparts/requestPortal/shared/approval/approvalSubmission.service.ts @@ -3,7 +3,7 @@ import { resolveRoute } from './approvalRoute.service'; import { ApprovalListService, IApproverToWrite } from './approvalList.service'; import { IMatrixRow } from './models/IMatrixRow'; import { ApprovalStatus, IResolveFieldValue } from './models/IApprovalRouteApi'; -import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses'; +import { APPROVAL_PHASE, REQUEST_STATUS, isSubmittedStatus } from './models/statuses'; // Планируемый маршрут для НОВОЙ заявки: resolve по email инициатора → строки матрицы. // status выбирает фазу: 'Approval' (согласующие, по умолчанию) или 'Execution' @@ -82,7 +82,11 @@ export async function writeRoute( // При ОТПРАВКЕ все шаги пишем «Ждёт очереди», затем активируем первый — // так активен ровно текущий шаг, остальные ждут. В черновике — статус заявки // (Черновик), без активации. - const submitted = status === REQUEST_STATUS.onApproval; + // + // Именно предикат, а не `status === onApproval`: отправка маршрута без + // согласующих даёт «Выполнение» (см. initialSubmitStatus), и при строгом + // сравнении такая заявка не активировала ни одного шага исполнения. + const submitted = isSubmittedStatus(status); const rowStatus = submitted ? REQUEST_STATUS.queued : status; // Смещение фазы исполнения — по МАКСИМАЛЬНОМУ номеру шага согласующих, а не по diff --git a/src/webparts/requestPortal/shared/approval/index.ts b/src/webparts/requestPortal/shared/approval/index.ts index 9f96dcc..9441042 100644 --- a/src/webparts/requestPortal/shared/approval/index.ts +++ b/src/webparts/requestPortal/shared/approval/index.ts @@ -8,6 +8,7 @@ export { EDITABLE_STATUSES, isEditableStatus, initialSubmitStatus, + isSubmittedStatus, } from './models/statuses'; export type { ISiteUser } from './models/ISiteUser'; export type { IMatrixRow } from './models/IMatrixRow'; diff --git a/src/webparts/requestPortal/shared/approval/models/statuses.test.ts b/src/webparts/requestPortal/shared/approval/models/statuses.test.ts index 30f8d72..d9f049f 100644 --- a/src/webparts/requestPortal/shared/approval/models/statuses.test.ts +++ b/src/webparts/requestPortal/shared/approval/models/statuses.test.ts @@ -5,7 +5,9 @@ import { EDITABLE_STATUSES, REQUEST_STATUS, REQUEST_STATUS_CHOICES, + initialSubmitStatus, isEditableStatus, + isSubmittedStatus, } from './statuses'; describe('isEditableStatus', () => { @@ -32,6 +34,43 @@ describe('isEditableStatus', () => { }); }); +describe('initialSubmitStatus', () => { + it('есть согласующие — заявка идёт на согласование', () => { + expect(initialSubmitStatus(2, 0)).toBe(REQUEST_STATUS.onApproval); + expect(initialSubmitStatus(2, 3)).toBe(REQUEST_STATUS.onApproval); + }); + + it('согласующих нет, но есть исполнители — сразу в исполнение', () => { + expect(initialSubmitStatus(0, 1)).toBe(REQUEST_STATUS.execution); + }); + + it('маршрута нет вовсе — заявка сразу согласована', () => { + expect(initialSubmitStatus(0, 0)).toBe(REQUEST_STATUS.approved); + }); +}); + +// writeRoute по этому предикату решает, писать строки как «Ждёт очереди» и +// активировать первую, или оставить как есть (черновик). Каждый статус, который +// возвращает initialSubmitStatus, ОБЯЗАН считаться отправкой: иначе строки +// маршрута получают недопустимый статус, а активации не происходит вовсе — +// именно так заявка без согласующих перестала попадать в очередь исполнения. +describe('isSubmittedStatus', () => { + it('любой результат initialSubmitStatus считается отправкой', () => { + [initialSubmitStatus(1, 0), initialSubmitStatus(0, 1), initialSubmitStatus(0, 0)].forEach((s) => + expect(isSubmittedStatus(s)).toBe(true) + ); + }); + + it('черновик отправкой не считается', () => { + expect(isSubmittedStatus(REQUEST_STATUS.draft)).toBe(false); + }); + + it('возврат и отклонение отправкой не считаются (маршрут не переактивируется)', () => { + expect(isSubmittedStatus(REQUEST_STATUS.returned)).toBe(false); + expect(isSubmittedStatus(REQUEST_STATUS.rejected)).toBe(false); + }); +}); + // Наборы Choice-значений держатся в синхроне с provisioning.json списков — // расхождение ломает запись (SharePoint отвергает значение вне Choice). describe('наборы Choice-значений', () => { diff --git a/src/webparts/requestPortal/shared/approval/models/statuses.ts b/src/webparts/requestPortal/shared/approval/models/statuses.ts index da186a7..1618583 100644 --- a/src/webparts/requestPortal/shared/approval/models/statuses.ts +++ b/src/webparts/requestPortal/shared/approval/models/statuses.ts @@ -61,6 +61,27 @@ export function initialSubmitStatus(approversCount: number, executorsCount: numb return REQUEST_STATUS.approved; } +/** + * Статус заявки означает «отправлена» (в отличие от черновика)? + * + * Нужен writeRoute: он по этому признаку решает, писать строки маршрута как + * «Ждёт очереди» + активировать первую, или сохранить их «как есть» (черновик). + * Раньше там стояло строгое `status === onApproval` — единственный статус, + * которым формы отправляли заявку. С появлением initialSubmitStatus отправка + * может дать «Выполнение» (маршрут без согласующих), и строгое сравнение молча + * ломалось: строки получали ApprovalStatus «Выполнение» (недопустимое значение + * для строки, см. APPROVAL_ROW_STATUS_CHOICES), а activateNextQueued не + * вызывался — ни одна строка не становилась «На исполнении», и заявка не + * попадала в очередь исполнителя. + */ +export function isSubmittedStatus(status: string): boolean { + return ( + status === REQUEST_STATUS.onApproval || + status === REQUEST_STATUS.execution || + status === REQUEST_STATUS.approved + ); +} + // Наборов значений ДВА, и они не совпадают — это разные сущности: // // 1) статус ЗАЯВКИ — колонка RequestStatus в списке «… — данные». Пишется