131e302f67
Tests / frontend-test (push) Successful in 1m18s
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/12
168 lines
8.9 KiB
TypeScript
168 lines
8.9 KiB
TypeScript
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||
import { APPROVAL_API_CLIENT_ID, getApprovalApiBaseUrl } from '../config';
|
||
import {
|
||
ApprovalStatus,
|
||
ICurrentListPartition,
|
||
IApiResponse,
|
||
IRequestType,
|
||
IRequestTypeListSeries,
|
||
IResolvedRoute,
|
||
IResolveFieldValue
|
||
} from './models/IApprovalRouteApi';
|
||
|
||
// Все обращения к внешнему approval-route API — только здесь.
|
||
// Клиент авторизуется через AadHttpClient (SSO по текущему пользователю).
|
||
async function client(context: WebPartContext): Promise<AadHttpClient> {
|
||
return context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
||
}
|
||
|
||
// GET /api/request-types — список типов заявок (id + имя). Пригодится, чтобы
|
||
// сопоставить форму с RequestTypeId, если он неизвестен заранее.
|
||
export async function getRequestTypes(context: WebPartContext): Promise<IRequestType[]> {
|
||
const c = await client(context);
|
||
const response: HttpClientResponse = await c.get(
|
||
`${getApprovalApiBaseUrl()}/api/request-types`,
|
||
AadHttpClient.configurations.v1
|
||
);
|
||
if (!response.ok) {
|
||
throw new Error(`request-types: ${response.status} ${response.statusText}`);
|
||
}
|
||
const body: IApiResponse<IRequestType[]> = await response.json();
|
||
return body.data ?? [];
|
||
}
|
||
|
||
/**
|
||
* GET /api/request-types/lists — серии партиций сразу по ВСЕМ типам заявок.
|
||
*
|
||
* Именно bulk-эндпоинт, а не по одному на тип: партиции нужны и для записи (куда
|
||
* писать), и для поиска (по каким спискам искать), причём поиск идёт сразу по
|
||
* всем типам — запрашивать серию отдельно на каждый тип означало бы N запросов
|
||
* на каждое открытие «Моих заявок».
|
||
*
|
||
* Типы без настроенного списка SharePoint в ответ не попадают.
|
||
*/
|
||
export async function getListPartitions(context: WebPartContext): Promise<IRequestTypeListSeries[]> {
|
||
const c = await client(context);
|
||
const response: HttpClientResponse = await c.get(
|
||
`${getApprovalApiBaseUrl()}/api/request-types/lists`,
|
||
AadHttpClient.configurations.v1
|
||
);
|
||
if (!response.ok) {
|
||
throw new Error(`request-types/lists: ${response.status} ${response.statusText}`);
|
||
}
|
||
const body: IApiResponse<IRequestTypeListSeries[]> = await response.json();
|
||
return body.data ?? [];
|
||
}
|
||
|
||
/**
|
||
* GET /api/request-types/{id}/lists/current — партиция, в которую нужно писать
|
||
* НОВУЮ заявку этого типа. Вызывать перед КАЖДЫМ созданием, не кешировать: этот
|
||
* запрос заодно проверяет порог элементов на бэкенде и ставит в очередь
|
||
* создание следующей партиции при достижении — без него архивация никогда не
|
||
* запустится, сколько бы заявок ни создали (bulk-эндпоинт lists, который
|
||
* портал и так грузит при onInit, только читает реестр и порог не проверяет).
|
||
*/
|
||
export async function getCurrentListPartition(
|
||
context: WebPartContext,
|
||
requestTypeId: number
|
||
): Promise<ICurrentListPartition> {
|
||
const c = await client(context);
|
||
const response: HttpClientResponse = await c.get(
|
||
`${getApprovalApiBaseUrl()}/api/request-types/${requestTypeId}/lists/current`,
|
||
AadHttpClient.configurations.v1
|
||
);
|
||
if (!response.ok) {
|
||
throw new Error(`request-types/${requestTypeId}/lists/current: ${response.status} ${response.statusText}`);
|
||
}
|
||
const body: IApiResponse<ICurrentListPartition> = await response.json();
|
||
return body.data;
|
||
}
|
||
|
||
// Кеш на уровне МОДУЛЯ (не useRef/useState) — список типов заявок админ меняет
|
||
// редко, а без кеша каждая форма и каждый переход в режим согласующего дёргали
|
||
// бы /api/request-types заново. Живёт до перезагрузки страницы — тот же принцип,
|
||
// что и у features/myRequests/pageCache.ts. Ошибку НЕ кешируем: если запрос
|
||
// упал (сеть, 401 на холодном старте), следующий вызов должен попробовать заново,
|
||
// а не молча повторять один и тот же провал до конца сессии.
|
||
let requestTypesPromise: Promise<IRequestType[]> | undefined;
|
||
|
||
export async function getRequestTypesCached(context: WebPartContext): Promise<IRequestType[]> {
|
||
if (!requestTypesPromise) {
|
||
requestTypesPromise = getRequestTypes(context).catch((e) => {
|
||
requestTypesPromise = undefined;
|
||
throw e;
|
||
});
|
||
}
|
||
return requestTypesPromise;
|
||
}
|
||
|
||
/**
|
||
* Числовой RequestTypeId по pageUrl (rptype) формы. Id типа заявки выдаётся
|
||
* сервером при регистрации типа в админке approval-route и НЕПРЕДСКАЗУЕМ
|
||
* заранее (см. README форм — «в старой pto-tracker было 1, при заведении типа
|
||
* в админке получил новый id 11»), а pageUrl задаём мы сами в data/requests.ts
|
||
* и он стабилен. Поэтому формы резолвят числовой id отсюда (через
|
||
* useRequestTypeId), а не хардкодят его.
|
||
*/
|
||
export async function getRequestTypeIdByPageUrl(context: WebPartContext, pageUrl: string): Promise<number> {
|
||
const types = await getRequestTypesCached(context);
|
||
const found = types.filter((t) => t.pageUrl === pageUrl)[0];
|
||
if (!found) {
|
||
throw new Error(
|
||
`Тип заявки с pageUrl «${pageUrl}» не найден в approval-route API — зарегистрируйте его в админке маршрутов (Типы заявок).`
|
||
);
|
||
}
|
||
return found.id;
|
||
}
|
||
|
||
// POST /api/template-routes/resolve — планируемый маршрут для типа заявки и
|
||
// пользователя. Бэкенд сам подставляет исполнителей шага (executorEmail/position)
|
||
// по ролям исходя из userEmail. Параметр status выбирает ФАЗУ маршрута:
|
||
// 'Approval' — согласующие (кто одобряет),
|
||
// 'Execution' — исполнители (кто выполняет после согласования).
|
||
// Один и тот же маршрут, разные наборы шагов.
|
||
//
|
||
// fieldValues — условие видимости шагов (см. IResolveFieldValue): для форм с
|
||
// шагами, зависящими от значения колонки списка (напр. WorkType), передать
|
||
// текущее значение поля здесь. Формы без условных шагов его не передают —
|
||
// бэкенд получает пустой массив, как раньше.
|
||
export async function resolveRoute(
|
||
context: WebPartContext,
|
||
requestTypeId: number,
|
||
userEmail: string,
|
||
status: ApprovalStatus = 'Approval',
|
||
// eslint-disable-next-line @rushstack/no-new-null -- null отправляется в теле запроса как «не задано», undefined в JSON.stringify пропал бы вовсе
|
||
departmentId: string | null = null,
|
||
fieldValues: IResolveFieldValue[] = [],
|
||
// eslint-disable-next-line @rushstack/no-new-null -- см. departmentId
|
||
projectId: string | null = null
|
||
): Promise<IResolvedRoute> {
|
||
const c = await client(context);
|
||
const url = `${getApprovalApiBaseUrl()}/api/template-routes/resolve`;
|
||
|
||
// Эндпоинт сменился с GET (query) на POST (JSON-тело).
|
||
// projectId (uuid проекта) — для форм, где согласующий зависит от проекта
|
||
// (напр. «руководитель проекта-источника» в СЗ на перемещение ТМЦ): бэкенд по
|
||
// нему подставляет исполнителя роли. Формы без привязки к проекту шлют null.
|
||
const response: HttpClientResponse = await c.post(url, AadHttpClient.configurations.v1, {
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Accept: 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
requestTypeId,
|
||
userEmail,
|
||
projectId: projectId || null,
|
||
status,
|
||
fieldValues,
|
||
departmentId: departmentId || null
|
||
})
|
||
});
|
||
if (!response.ok) {
|
||
throw new Error(`resolve: ${response.status} ${response.statusText}`);
|
||
}
|
||
const body: IApiResponse<IResolvedRoute> = await response.json();
|
||
return body.data;
|
||
}
|