feat: добавил поле организация во все заявки, а также расширил компонет выбора даты для ui-kit
This commit is contained in:
@@ -17,19 +17,46 @@ async function client(context: WebPartContext): Promise<AadHttpClient> {
|
||||
return context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
||||
}
|
||||
|
||||
// Ретрай-политика на сетевые сбои — тот же принцип и те же по порядку величины
|
||||
// значения, что и у RETRY в shared/pnp/pnpjs.ts (там для PnPjs/SharePoint).
|
||||
const NETWORK_RETRY_ATTEMPTS = 2;
|
||||
const NETWORK_RETRY_INTERVAL_MS = 1000;
|
||||
|
||||
/**
|
||||
* Мягкий ретрай на СЕТЕВЫЕ сбои (fetch не долетел вовсе — TypeError «Failed to
|
||||
* fetch»/«NetworkError», не HTTP-статус). Тот же принцип, что и
|
||||
* BrowserFetchWithRetry у PnPjs-клиента (shared/pnp/pnpjs.ts) — там ретрай уже
|
||||
* есть, а этот внешний API его был лишён, из-за чего случайная заминка сети или
|
||||
* холодный старт бэкенда сразу превращались в необратимое «Failed to fetch» в
|
||||
* UI матрицы. HTTP-ошибки (404/401/500 — throw Error ниже по коду) НЕ ретраим:
|
||||
* повтор того же запроса их не лечит, только тянет время до сообщения об ошибке.
|
||||
*/
|
||||
async function withNetworkRetry<T>(fn: () => Promise<T>): Promise<T> {
|
||||
for (let attempt = 0; ; attempt += 1) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
if (attempt >= NETWORK_RETRY_ATTEMPTS || !(e instanceof TypeError)) throw e;
|
||||
await new Promise((resolve) => setTimeout(resolve, NETWORK_RETRY_INTERVAL_MS * (attempt + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ?? [];
|
||||
return withNetworkRetry(async () => {
|
||||
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 ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,16 +70,18 @@ export async function getRequestTypes(context: WebPartContext): Promise<IRequest
|
||||
* Типы без настроенного списка 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 ?? [];
|
||||
return withNetworkRetry(async () => {
|
||||
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 ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,16 +96,18 @@ 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;
|
||||
return withNetworkRetry(async () => {
|
||||
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) — список типов заявок админ меняет
|
||||
@@ -138,30 +169,32 @@ export async function resolveRoute(
|
||||
// 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`;
|
||||
return withNetworkRetry(async () => {
|
||||
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
|
||||
})
|
||||
// Эндпоинт сменился с 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;
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`resolve: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body: IApiResponse<IResolvedRoute> = await response.json();
|
||||
return body.data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user