fix:передаю условия маршрута по компании в resolve и починил пагинацию /Project
Tests / frontend-test (push) Failing after 14s
Tests / frontend-test (push) Failing after 14s
This commit is contained in:
@@ -126,20 +126,42 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
|||||||
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
||||||
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
||||||
const requestTypeId = requestTypeIdState.id;
|
const requestTypeId = requestTypeIdState.id;
|
||||||
|
// Маршрут для этого типа заявки зависит от проекта (ByProject) — без projectId
|
||||||
|
// бэкенд отвечает resolve: 404 «…и признака «по проекту» не найден» (поле
|
||||||
|
// Project изначально задумывалось справочным, но выяснилось, что маршрут
|
||||||
|
// всё-таки требует projectId, как в RecruitingSelection/TmcTransfer).
|
||||||
|
const projectId = data.ProjectId || null;
|
||||||
|
const matrixReady = !!editId || (!!projectId && !!requestTypeId);
|
||||||
|
// Несколько шагов маршрута условны по компании (см. панель «Маршруты»:
|
||||||
|
// «CompaniesJson = TOO QAZGEOMAX», «CompaniesJson = TOO CT Global», шаг с
|
||||||
|
// сразу ДВУМЯ условиями одновременно). Companies — мультивыбор → ОДНА запись
|
||||||
|
// fieldValues, value — имена выбранных компаний ЧЕРЕЗ ПРОБЕЛ (не JSON, не
|
||||||
|
// массив — ResolveFieldValueDto на бэкенде принимает только строку, массив
|
||||||
|
// даёт 400; сериализованный JSON не совпадает с условием при сравнении).
|
||||||
|
// Плюс шаг «Роль: Планировщик» условен по названию проекта («Project не
|
||||||
|
// содержит офис») — сюда идёт ТЕКСТ (data.Project), а не projectId (GUID),
|
||||||
|
// который уже передаётся отдельным параметром resolve.
|
||||||
|
const companiesFieldValue = data.Companies.map((c) => c.Name).sort().join(' ');
|
||||||
|
const fieldValues = useMemo(() => {
|
||||||
|
const companies = companiesFieldValue ? [{ fieldName: 'CompaniesJson', value: companiesFieldValue }] : [];
|
||||||
|
return data.Project ? [...companies, { fieldName: 'Project', value: data.Project }] : companies;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- companiesFieldValue — стабильный слепок data.Companies
|
||||||
|
}, [companiesFieldValue, data.Project]);
|
||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval
|
? approval
|
||||||
.getMatrixForRequest(editId)
|
.getMatrixForRequest(editId)
|
||||||
.then((rows) => rows.filter((r) => r.phase !== APPROVAL_PHASE.execution))
|
.then((rows) => rows.filter((r) => r.phase !== APPROVAL_PHASE.execution))
|
||||||
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval'),
|
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, fieldValues, projectId),
|
||||||
[context, userEmail, approval, editId, requestTypeId]
|
[context, userEmail, approval, editId, requestTypeId, fieldValues, projectId]
|
||||||
);
|
);
|
||||||
const matrix = useMatrixEditor(
|
const matrix = useMatrixEditor(
|
||||||
loadMatrix,
|
loadMatrix,
|
||||||
`${userEmail}-${editId ?? 'new'}`,
|
// projectId/companies в ключе → при их смене маршрут перезапрашивается.
|
||||||
|
`${userEmail}-${editId ?? 'new'}-${projectId}-${companiesFieldValue}`,
|
||||||
!readOnly,
|
!readOnly,
|
||||||
!!editId || !!requestTypeId,
|
matrixReady,
|
||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -150,14 +172,14 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
|||||||
? approval
|
? approval
|
||||||
.getMatrixForRequest(editId)
|
.getMatrixForRequest(editId)
|
||||||
.then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution))
|
.then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution))
|
||||||
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Execution'),
|
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Execution', null, fieldValues, projectId),
|
||||||
[context, userEmail, approval, editId, requestTypeId]
|
[context, userEmail, approval, editId, requestTypeId, fieldValues, projectId]
|
||||||
);
|
);
|
||||||
const executors = useMatrixEditor(
|
const executors = useMatrixEditor(
|
||||||
loadExecutors,
|
loadExecutors,
|
||||||
`${userEmail}-${editId ?? 'new'}`,
|
`${userEmail}-${editId ?? 'new'}-${projectId}-${companiesFieldValue}`,
|
||||||
false,
|
false,
|
||||||
!!editId || !!requestTypeId,
|
matrixReady,
|
||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -353,7 +375,12 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
|||||||
</CollapseSection>
|
</CollapseSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ApprovalMatrix editor={matrix} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix
|
||||||
|
editor={matrix}
|
||||||
|
search={(q) => approval.searchUsers(q)}
|
||||||
|
waiting={!matrixReady}
|
||||||
|
waitingText="Выберите «Проект», чтобы построить маршрут согласования."
|
||||||
|
/>
|
||||||
|
|
||||||
{!executors.loading && executors.rows.length > 0 && (
|
{!executors.loading && executors.rows.length > 0 && (
|
||||||
<ApprovalMatrix
|
<ApprovalMatrix
|
||||||
|
|||||||
@@ -116,17 +116,29 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Новая заявка: матрицу резолвим только когда есть и тип, и проект — иначе
|
// Новая заявка: матрицу резолвим только когда есть и тип, и проект — иначе
|
||||||
// 404 «маршрут не найден». При редактировании читаем уже записанные строки.
|
// 404 «маршрут не найден». При редактировании читаем уже записанные строки.
|
||||||
const matrixReady = !!editId || (!!projectId && !!requestTypeId);
|
const matrixReady = !!editId || (!!projectId && !!requestTypeId);
|
||||||
|
// Условные шаги маршрута (см. [[request-portal-fieldvalues-routing]] — тот же
|
||||||
|
// разбор на RecruitingHire): fieldName = internalName колонки. Companies —
|
||||||
|
// мультивыбор → ОДНА запись fieldValues, value — имена выбранных компаний
|
||||||
|
// ЧЕРЕЗ ПРОБЕЛ (не JSON, не массив — ResolveFieldValueDto на бэкенде
|
||||||
|
// принимает только строку, массив даёт 400). ObjectProject — текст названия
|
||||||
|
// проекта (не ObjectProjectId/GUID, тот уже уходит отдельным параметром projectId).
|
||||||
|
const companiesFieldValue = data.Companies.map((c) => c.Name).sort().join(' ');
|
||||||
|
const fieldValues = useMemo(() => {
|
||||||
|
const companies = companiesFieldValue ? [{ fieldName: 'CompaniesJson', value: companiesFieldValue }] : [];
|
||||||
|
return data.ObjectProject ? [...companies, { fieldName: 'ObjectProject', value: data.ObjectProject }] : companies;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- companiesFieldValue — стабильный слепок data.Companies
|
||||||
|
}, [companiesFieldValue, data.ObjectProject]);
|
||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
editId
|
editId
|
||||||
? approval.getMatrixForRequest(editId)
|
? approval.getMatrixForRequest(editId)
|
||||||
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, undefined, projectId),
|
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', null, fieldValues, projectId),
|
||||||
[context, userEmail, approval, editId, requestTypeId, projectId]
|
[context, userEmail, approval, editId, requestTypeId, fieldValues, projectId]
|
||||||
);
|
);
|
||||||
const matrix = useMatrixEditor(
|
const matrix = useMatrixEditor(
|
||||||
loadMatrix,
|
loadMatrix,
|
||||||
// projectId в ключе → при смене проекта маршрут перезапрашивается.
|
// projectId/companies в ключе → при их смене маршрут перезапрашивается.
|
||||||
`${userEmail}-${editId ?? 'new'}-${projectId}`,
|
`${userEmail}-${editId ?? 'new'}-${projectId}-${companiesFieldValue}`,
|
||||||
!readOnly,
|
!readOnly,
|
||||||
matrixReady,
|
matrixReady,
|
||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
|
|||||||
@@ -71,7 +71,10 @@ export interface ICurrentListPartition {
|
|||||||
// «Показывать шаг только при выполнении условия», сравнение «Поле списка»/
|
// «Показывать шаг только при выполнении условия», сравнение «Поле списка»/
|
||||||
// «Оператор»/«Значение»). fieldName — внутреннее имя колонки списка (напр.
|
// «Оператор»/«Значение»). fieldName — внутреннее имя колонки списка (напр.
|
||||||
// "WorkType"), value — фактическое значение из формы (напр. "Создание
|
// "WorkType"), value — фактическое значение из формы (напр. "Создание
|
||||||
// пользователя в 1С"), с которым бэкенд сравнивает эту колонку.
|
// пользователя в 1С"), с которым бэкенд сравнивает эту колонку. ТОЛЬКО строка —
|
||||||
|
// бэкенд (ResolveFieldValueDto) не принимает массив в value (400 при попытке,
|
||||||
|
// см. [[request-portal-fieldvalues-routing]]). Для мультизначных полей (напр.
|
||||||
|
// CompaniesJson) — ОТДЕЛЬНАЯ запись с этим fieldName на КАЖДОЕ значение.
|
||||||
export interface IResolveFieldValue {
|
export interface IResolveFieldValue {
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@@ -25,7 +25,16 @@ export interface IProject {
|
|||||||
deleted: boolean;
|
deleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обёртка ответа /Project. */
|
/**
|
||||||
|
* Обёртка ответа /Project. Бэкенд игнорирует запрошенный pageSize и всегда
|
||||||
|
* отдаёт свой собственный размер страницы (напр. 100) — поэтому для остановки
|
||||||
|
* пагинации нужно сравнивать `page` с `totalPages` ИЗ ОТВЕТА, а не с тем, что
|
||||||
|
* запросили сами (см. projects.service.ts).
|
||||||
|
*/
|
||||||
export interface IProjectListResponse {
|
export interface IProjectListResponse {
|
||||||
items: IProject[];
|
items: IProject[];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalCount: number;
|
||||||
|
totalPages: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { IProject, IProjectListResponse } from './models/IProject';
|
|||||||
// ПАГИНИРОВАН, поэтому тянем ВСЕ страницы (иначе проекты за пределами первой
|
// ПАГИНИРОВАН, поэтому тянем ВСЕ страницы (иначе проекты за пределами первой
|
||||||
// страницы не находятся в поиске пикера). Авторизация та же (AadHttpClient +
|
// страницы не находятся в поиске пикера). Авторизация та же (AadHttpClient +
|
||||||
// APPROVAL_API_CLIENT_ID, SSO текущего пользователя).
|
// APPROVAL_API_CLIENT_ID, SSO текущего пользователя).
|
||||||
const PAGE_SIZE = 500;
|
const PAGE_SIZE = 500; // бэкенд может отдать меньше (см. IProjectListResponse) — не влияет на остановку цикла
|
||||||
const MAX_PAGES = 50; // страховка от бесконечного цикла (до 25 000 проектов)
|
const MAX_PAGES = 50; // страховка от бесконечного цикла, не привязана к PAGE_SIZE
|
||||||
|
|
||||||
async function fetchProjects(context: WebPartContext): Promise<IProject[]> {
|
async function fetchProjects(context: WebPartContext): Promise<IProject[]> {
|
||||||
const client = await context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
const client = await context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
||||||
@@ -24,8 +24,12 @@ async function fetchProjects(context: WebPartContext): Promise<IProject[]> {
|
|||||||
const body: IProjectListResponse = await response.json();
|
const body: IProjectListResponse = await response.json();
|
||||||
const items = body.items ?? [];
|
const items = body.items ?? [];
|
||||||
all.push(...items);
|
all.push(...items);
|
||||||
// Последняя страница — вернулось меньше запрошенного размера.
|
// Бэкенд игнорирует наш pageSize и отдаёт свой (напр. 100) — сравнивать
|
||||||
if (items.length < PAGE_SIZE) break;
|
// items.length с ЗАПРОШЕННЫМ PAGE_SIZE нельзя (он почти всегда больше
|
||||||
|
// реального размера страницы, цикл остановился бы после первой же
|
||||||
|
// страницы). Останавливаемся по totalPages ИЗ ОТВЕТА, а items.length===0 —
|
||||||
|
// страховка на случай, если бэкенд когда-нибудь перестанет отдавать totalPages.
|
||||||
|
if (items.length === 0 || (body.totalPages && page >= body.totalPages)) break;
|
||||||
}
|
}
|
||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user