небольшая оптимизация трасировка и починка обновления принудительного
This commit is contained in:
@@ -392,16 +392,20 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const load = async (): Promise<void> => {
|
const load = async (forceRender = false): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const fresh = await fetchPage();
|
const fresh = await fetchPage();
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
// Перерисовываем, только если данные реально изменились — иначе при
|
// Перерисовываем, только если данные реально изменились — иначе при
|
||||||
// каждом возврате на вкладку таблица моргала бы, перерисовываясь тем же
|
// каждом возврате на вкладку таблица моргала бы, перерисовываясь тем же
|
||||||
// самым содержимым.
|
// самым содержимым. forceRender — исключение: строки уже спрятаны со
|
||||||
|
// экрана (см. force ниже), и их обязательно нужно вернуть, даже если
|
||||||
|
// свежий ответ побайтово совпал с тем, что было в кеше, — иначе таблица
|
||||||
|
// так и останется в состоянии «Загрузка…», хотя данные давно пришли
|
||||||
|
// (баг: ушёл на этом самом совпадении сигнатур, найден 2026-08-18).
|
||||||
const prev = readPage(cacheKey);
|
const prev = readPage(cacheKey);
|
||||||
if (!prev || pageSignature(prev) !== pageSignature(fresh)) {
|
if (forceRender || !prev || pageSignature(prev) !== pageSignature(fresh)) {
|
||||||
writePage(cacheKey, fresh);
|
writePage(cacheKey, fresh);
|
||||||
setRows(fresh.rows);
|
setRows(fresh.rows);
|
||||||
setTotalRows(fresh.totalRows);
|
setTotalRows(fresh.totalRows);
|
||||||
@@ -419,12 +423,12 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
const unsubscribe = onRequestsChanged((force) => {
|
const unsubscribe = onRequestsChanged((force) => {
|
||||||
// Явный повторный клик по уже открытому пункту меню — пользователь ждёт
|
// Явный повторный клик по уже открытому пункту меню — пользователь ждёт
|
||||||
// видимого обновления, а не молчаливой подмены данных за спиной. Прежние
|
// видимого обновления, а не молчаливой подмены данных за спиной. Прежние
|
||||||
// строки прячем сразу; свежие подставит load().
|
// строки прячем сразу; свежие подставит load(force).
|
||||||
if (force) {
|
if (force) {
|
||||||
setRows(undefined);
|
setRows(undefined);
|
||||||
setLoadedKey('');
|
setLoadedKey('');
|
||||||
}
|
}
|
||||||
load().catch(() => undefined);
|
load(force).catch(() => undefined);
|
||||||
});
|
});
|
||||||
// Пока вкладка браузера скрыта, обновляться незачем — при возврате к ней
|
// Пока вкладка браузера скрыта, обновляться незачем — при возврате к ней
|
||||||
// сработает ближайший тик.
|
// сработает ближайший тик.
|
||||||
|
|||||||
@@ -182,13 +182,16 @@ export const SearchView: React.FC<ISearchViewProps> = ({
|
|||||||
setRows(cached ? cached.rows : undefined);
|
setRows(cached ? cached.rows : undefined);
|
||||||
setTotalRows(cached ? cached.totalRows : 0);
|
setTotalRows(cached ? cached.totalRows : 0);
|
||||||
|
|
||||||
const load = async (): Promise<void> => {
|
const load = async (forceRender = false): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const fresh = await searchMyRequests(context, types, debouncedQuery);
|
const fresh = await searchMyRequests(context, types, debouncedQuery);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
applyKnownAuthors(fresh.rows, authors.current);
|
applyKnownAuthors(fresh.rows, authors.current);
|
||||||
const prev = readPage(cacheKey);
|
const prev = readPage(cacheKey);
|
||||||
if (!prev || pageSignature(prev) !== pageSignature(fresh)) {
|
// forceRender — строки уже спрятаны (см. force ниже) и обязаны
|
||||||
|
// вернуться, даже если свежий ответ совпал с прежним кешем: иначе
|
||||||
|
// экран так и останется в «Ищем…» (см. тот же баг в RequestsTableView).
|
||||||
|
if (forceRender || !prev || pageSignature(prev) !== pageSignature(fresh)) {
|
||||||
writePage(cacheKey, fresh);
|
writePage(cacheKey, fresh);
|
||||||
setRows(fresh.rows);
|
setRows(fresh.rows);
|
||||||
setTotalRows(fresh.totalRows);
|
setTotalRows(fresh.totalRows);
|
||||||
@@ -202,7 +205,7 @@ export const SearchView: React.FC<ISearchViewProps> = ({
|
|||||||
load().catch(() => undefined);
|
load().catch(() => undefined);
|
||||||
const unsubscribe = onRequestsChanged((force) => {
|
const unsubscribe = onRequestsChanged((force) => {
|
||||||
if (force) setRows(undefined);
|
if (force) setRows(undefined);
|
||||||
load().catch(() => undefined);
|
load(force).catch(() => undefined);
|
||||||
});
|
});
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
if (!document.hidden) load().catch(() => undefined);
|
if (!document.hidden) load().catch(() => undefined);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { effectiveUserEmail } from '../../shared/impersonation';
|
|||||||
import { IMyRequest } from './models/IMyRequest';
|
import { IMyRequest } from './models/IMyRequest';
|
||||||
import { IMyRequestsTypeDebug } from './models/IMyRequestsDebug';
|
import { IMyRequestsTypeDebug } from './models/IMyRequestsDebug';
|
||||||
import { traceLinkedTypes, traceResultRows, traceSearchHits } from './searchTrace';
|
import { traceLinkedTypes, traceResultRows, traceSearchHits } from './searchTrace';
|
||||||
|
import { traced } from './perfTrace';
|
||||||
|
|
||||||
// Список данных «... — данные» → список согласования «... — согласование»
|
// Список данных «... — данные» → список согласования «... — согласование»
|
||||||
// (конвенция именования; см. forms/*/provisioning.json и forms/*/approval/provisioning.json).
|
// (конвенция именования; см. forms/*/provisioning.json и forms/*/approval/provisioning.json).
|
||||||
@@ -161,7 +162,9 @@ async function buildPendingIndex(
|
|||||||
const tokenEmail = currentUserEmail(context);
|
const tokenEmail = currentUserEmail(context);
|
||||||
|
|
||||||
const perType = await Promise.all(
|
const perType = await Promise.all(
|
||||||
types.map((t) => indexApprovalsForType(context, t, tokenEmail, scope, statuses))
|
types.map((t) =>
|
||||||
|
traced(`indexApprovalsForType:${t.name}`, () => indexApprovalsForType(context, t, tokenEmail, scope, statuses))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Свежие сверху — типы объединяются в один список, сортировать нужно после слияния.
|
// Свежие сверху — типы объединяются в один список, сортировать нужно после слияния.
|
||||||
@@ -232,8 +235,8 @@ export async function getNavCounts(context: WebPartContext): Promise<INavCounts>
|
|||||||
const types = await getLinkedTypes(context);
|
const types = await getLinkedTypes(context);
|
||||||
|
|
||||||
const [turn, rework] = await Promise.all([
|
const [turn, rework] = await Promise.all([
|
||||||
buildPendingIndex(context, 'myTurn', MY_TURN_STATUSES),
|
traced('buildPendingIndex:myTurn', () => buildPendingIndex(context, 'myTurn', MY_TURN_STATUSES)),
|
||||||
getOwnRequestsByStatus(context, types, REQUEST_STATUS.returned, 1),
|
traced('getOwnRequestsByStatus:rework', () => getOwnRequestsByStatus(context, types, REQUEST_STATUS.returned, 1)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -289,12 +292,28 @@ async function indexApprovalsForType(
|
|||||||
// партиций не загрузился — тогда ведём себя как раньше, по одному списку.
|
// партиций не загрузился — тогда ведём себя как раньше, по одному списку.
|
||||||
const targets = listTargetsForType(type);
|
const targets = listTargetsForType(type);
|
||||||
|
|
||||||
|
// Партиции независимы друг от друга (разные списки, разные HTTP-запросы) —
|
||||||
|
// читаем ВСЕ параллельно, а не по очереди. Раньше это был последовательный
|
||||||
|
// for-await, и у типа с несколькими партициями (архив накопился) время росло
|
||||||
|
// линейно с их числом: 3 партиции — втрое дольше одной, хотя каждая опрашивает
|
||||||
|
// СВОЙ список и ничего не ждёт от соседних (см. трассировку 2026-08-18:
|
||||||
|
// «Обходной лист» с 3 партициями — 7.5с, тогда как каждая партиция сама по
|
||||||
|
// себе — 2-3с).
|
||||||
|
const parts = await Promise.all(
|
||||||
|
targets.map((target) =>
|
||||||
|
traced(`indexApprovalsForPartition:${type.name}#${target.listId}`, () =>
|
||||||
|
indexApprovalsForPartition(context, type, target, tokenEmail, scope, statuses)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const rows: IMyRequest[] = [];
|
const rows: IMyRequest[] = [];
|
||||||
const approvalTitles: string[] = [];
|
const approvalTitles: string[] = [];
|
||||||
|
|
||||||
for (const target of targets) {
|
// Порядок Promise.all совпадает с порядком targets — «первый встреченный»
|
||||||
const part = await indexApprovalsForPartition(context, type, target, tokenEmail, scope, statuses);
|
// результат (approvalSkippedReason/approvalError) не изменился относительно
|
||||||
|
// прежнего последовательного перебора.
|
||||||
|
for (const part of parts) {
|
||||||
rows.push(...part.rows);
|
rows.push(...part.rows);
|
||||||
if (part.approvalListTitle) approvalTitles.push(part.approvalListTitle);
|
if (part.approvalListTitle) approvalTitles.push(part.approvalListTitle);
|
||||||
if (part.approvalSkippedReason && !debug.approvalSkippedReason) {
|
if (part.approvalSkippedReason && !debug.approvalSkippedReason) {
|
||||||
@@ -333,7 +352,7 @@ async function indexApprovalsForPartition(
|
|||||||
const sp = spForSite(target.siteUrl, context);
|
const sp = spForSite(target.siteUrl, context);
|
||||||
const list = sp.web.getList(target.listId);
|
const list = sp.web.getList(target.listId);
|
||||||
|
|
||||||
const dataListInfo = await list.select('Title')();
|
const dataListInfo = await traced(`dataListTitle:${type.name}#${target.listId}`, () => list.select('Title')());
|
||||||
const dataTitle = (dataListInfo.Title as string) || '';
|
const dataTitle = (dataListInfo.Title as string) || '';
|
||||||
if (!DATA_LIST_SUFFIX.test(dataTitle)) {
|
if (!DATA_LIST_SUFFIX.test(dataTitle)) {
|
||||||
debug.approvalSkippedReason = `«${dataTitle}» не оканчивается на «— данные» — список согласования не ищем`;
|
debug.approvalSkippedReason = `«${dataTitle}» не оканчивается на «— данные» — список согласования не ищем`;
|
||||||
@@ -371,14 +390,22 @@ async function indexApprovalsForPartition(
|
|||||||
// REST-довесок сразу отдаёт только нужные строки, без постфильтрации в JS.
|
// REST-довесок сразу отдаёт только нужные строки, без постфильтрации в JS.
|
||||||
const statusFilter =
|
const statusFilter =
|
||||||
statuses.length > 0 ? ` and (${statuses.map((s) => `ApprovalStatus eq '${s}'`).join(' or ')})` : '';
|
statuses.length > 0 ? ` and (${statuses.map((s) => `ApprovalStatus eq '${s}'`).join(' or ')})` : '';
|
||||||
const recentRows = (await approvalList.items
|
// recentRows (REST) и searchRows (Search) — независимые источники
|
||||||
|
// кандидатов, объединяются только ниже (seenIds); одно не ждёт другое,
|
||||||
|
// поэтому запускаем их параллельно, а не по очереди.
|
||||||
|
const [recentRows, searchRows] = (await Promise.all([
|
||||||
|
traced(`recentRowsRest:${type.name}#${target.listId}`, () =>
|
||||||
|
approvalList.items
|
||||||
.filter(`TansuSiteApproverId eq ${approver.Id} and Modified ge '${cutoffIso}'${statusFilter}`)
|
.filter(`TansuSiteApproverId eq ${approver.Id} and Modified ge '${cutoffIso}'${statusFilter}`)
|
||||||
.select('Id', 'TicketId', 'ApprovalStatus')
|
.select('Id', 'TicketId', 'ApprovalStatus')
|
||||||
.top(2000)()) as Array<Record<string, unknown>>;
|
.top(2000)()
|
||||||
|
),
|
||||||
// Статусы уходят и в KQL: сервер сразу отдаёт только нужные строки,
|
// Статусы уходят и в KQL: сервер сразу отдаёт только нужные строки,
|
||||||
// а не всё, где пользователь есть в маршруте.
|
// а не всё, где пользователь есть в маршруте.
|
||||||
const searchRows = await getInvolvedRowsViaSearch(context, type, approvalList, statuses);
|
traced(`getInvolvedRowsViaSearch:${type.name}#${target.listId}`, () =>
|
||||||
|
getInvolvedRowsViaSearch(context, type, approvalList, statuses)
|
||||||
|
),
|
||||||
|
])) as [Array<Record<string, unknown>>, Array<Record<string, unknown>>];
|
||||||
const seenIds = new Set(recentRows.map((r) => r.Id));
|
const seenIds = new Set(recentRows.map((r) => r.Id));
|
||||||
stepRows = recentRows.concat(searchRows.filter((r) => !seenIds.has(r.Id)));
|
stepRows = recentRows.concat(searchRows.filter((r) => !seenIds.has(r.Id)));
|
||||||
} else {
|
} else {
|
||||||
@@ -389,7 +416,9 @@ async function indexApprovalsForPartition(
|
|||||||
// давал неустойчивый результат (заявка то находилась, то нет, в
|
// давал неустойчивый результат (заявка то находилась, то нет, в
|
||||||
// зависимости от того, успел ли Search её проиндексировать к моменту
|
// зависимости от того, успел ли Search её проиндексировать к моменту
|
||||||
// REST-окна).
|
// REST-окна).
|
||||||
stepRows = await getInvolvedRowsViaSearch(context, type, approvalList, statuses);
|
stepRows = await traced(`getInvolvedRowsViaSearch:${type.name}#${target.listId}`, () =>
|
||||||
|
getInvolvedRowsViaSearch(context, type, approvalList, statuses)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.approvalRowCount = stepRows.length;
|
debug.approvalRowCount = stepRows.length;
|
||||||
@@ -416,10 +445,12 @@ async function indexApprovalsForPartition(
|
|||||||
// Заголовок/статус/дата заявки — отдельным запросом к списку ДАННЫХ (см.
|
// Заголовок/статус/дата заявки — отдельным запросом к списку ДАННЫХ (см.
|
||||||
// комментарий у функции: через Lookup-expand это не отдаётся).
|
// комментарий у функции: через Lookup-expand это не отдаётся).
|
||||||
const ticketFilter = ticketIds.map((id) => `Id eq ${id}`).join(' or ');
|
const ticketFilter = ticketIds.map((id) => `Id eq ${id}`).join(' or ');
|
||||||
const ticketRows = (await list.items
|
const ticketRows = (await traced(`ticketRowsRest:${type.name}#${target.listId}`, () =>
|
||||||
|
list.items
|
||||||
.filter(ticketFilter)
|
.filter(ticketFilter)
|
||||||
.select('Id', 'Title', 'RequestStatus', 'Created')
|
.select('Id', 'Title', 'RequestStatus', 'Created')
|
||||||
.top(ticketIds.length)()) as Array<Record<string, unknown>>;
|
.top(ticketIds.length)()
|
||||||
|
)) as Array<Record<string, unknown>>;
|
||||||
|
|
||||||
debug.rows = ticketRows.map((r) => ({
|
debug.rows = ticketRows.map((r) => ({
|
||||||
...toMyRequest(r, undefined, type, 'approval', target),
|
...toMyRequest(r, undefined, type, 'approval', target),
|
||||||
@@ -620,14 +651,16 @@ async function fetchAllSearchRows(
|
|||||||
let startRow = 0;
|
let startRow = 0;
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const results = await sp.search({
|
const results = await traced(`search:${label}#${startRow}`, () =>
|
||||||
|
sp.search({
|
||||||
Querytext: kql,
|
Querytext: kql,
|
||||||
RowLimit: SEARCH_PAGE_SIZE,
|
RowLimit: SEARCH_PAGE_SIZE,
|
||||||
StartRow: startRow,
|
StartRow: startRow,
|
||||||
SelectProperties: selectProperties,
|
SelectProperties: selectProperties,
|
||||||
TrimDuplicates: false,
|
TrimDuplicates: false,
|
||||||
...(sortList ? { SortList: sortList } : {}),
|
...(sortList ? { SortList: sortList } : {}),
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const page = results.PrimarySearchResults as Array<Record<string, unknown>>;
|
const page = results.PrimarySearchResults as Array<Record<string, unknown>>;
|
||||||
rows.push(...page);
|
rows.push(...page);
|
||||||
@@ -858,11 +891,13 @@ async function loadByIds(
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
chunk(ids, REST_ID_FILTER_CHUNK).map(async (idsChunk) => {
|
chunk(ids, REST_ID_FILTER_CHUNK).map(async (idsChunk) => {
|
||||||
const rows = (await list.items
|
const rows = (await traced(`loadByIds:${kind}:${type.name}#${target.listId}`, () =>
|
||||||
|
list.items
|
||||||
.filter(idsChunk.map((id) => `Id eq ${id}`).join(' or '))
|
.filter(idsChunk.map((id) => `Id eq ${id}`).join(' or '))
|
||||||
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
.select('Id', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||||
.expand('Author')
|
.expand('Author')
|
||||||
.top(idsChunk.length)()) as Array<Record<string, unknown>>;
|
.top(idsChunk.length)()
|
||||||
|
)) as Array<Record<string, unknown>>;
|
||||||
rows.forEach((r) => {
|
rows.forEach((r) => {
|
||||||
const author = r.Author as { EMail?: string; Title?: string } | undefined;
|
const author = r.Author as { EMail?: string; Title?: string } | undefined;
|
||||||
byKey.set(
|
byKey.set(
|
||||||
@@ -981,14 +1016,15 @@ export async function getOwnRequests(
|
|||||||
): Promise<IOwnRequestsPage> {
|
): Promise<IOwnRequestsPage> {
|
||||||
if (types.length === 0) return { rows: [], totalRows: 0 };
|
if (types.length === 0) return { rows: [], totalRows: 0 };
|
||||||
|
|
||||||
const searchPage = await searchOwnRequests(context, types, '', page);
|
const searchPage = await traced('searchOwnRequests', () => searchOwnRequests(context, types, '', page));
|
||||||
|
|
||||||
if (page !== 1) return searchPage;
|
if (page !== 1) return searchPage;
|
||||||
|
|
||||||
const freshRows = (await Promise.all(types.map((t) => getFreshOwnRowsForType(context, t)))).reduce(
|
const freshRows = (
|
||||||
(all, r) => all.concat(r),
|
await Promise.all(
|
||||||
[] as IMyRequest[]
|
types.map((t) => traced(`getFreshOwnRowsForType:${t.name}`, () => getFreshOwnRowsForType(context, t)))
|
||||||
);
|
)
|
||||||
|
).reduce((all, r) => all.concat(r), [] as IMyRequest[]);
|
||||||
|
|
||||||
const searchKeys = new Set(searchPage.rows.map((r) => rowKey(r)));
|
const searchKeys = new Set(searchPage.rows.map((r) => rowKey(r)));
|
||||||
const onlyFresh = freshRows.filter((r) => !searchKeys.has(rowKey(r)));
|
const onlyFresh = freshRows.filter((r) => !searchKeys.has(rowKey(r)));
|
||||||
@@ -1023,7 +1059,8 @@ export async function searchOwnRequests(
|
|||||||
if (text) clauses.push(text);
|
if (text) clauses.push(text);
|
||||||
const kql = clauses.join(' AND ');
|
const kql = clauses.join(' AND ');
|
||||||
|
|
||||||
const results = await sp.search({
|
const results = await traced('search:searchOwnRequests', () =>
|
||||||
|
sp.search({
|
||||||
Querytext: kql,
|
Querytext: kql,
|
||||||
RowLimit: PAGE_SIZE,
|
RowLimit: PAGE_SIZE,
|
||||||
StartRow: (page - 1) * PAGE_SIZE,
|
StartRow: (page - 1) * PAGE_SIZE,
|
||||||
@@ -1031,12 +1068,15 @@ export async function searchOwnRequests(
|
|||||||
TrimDuplicates: false,
|
TrimDuplicates: false,
|
||||||
// Свежие сверху (по умолчанию Search сортирует по релевантности, не по дате).
|
// Свежие сверху (по умолчанию Search сортирует по релевантности, не по дате).
|
||||||
SortList: [{ Property: 'Created', Direction: 1 }],
|
SortList: [{ Property: 'Created', Direction: 1 }],
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const totalRows = results.TotalRows || 0;
|
const totalRows = results.TotalRows || 0;
|
||||||
const entries = hitsToEntries(results, types);
|
const entries = hitsToEntries(results, types);
|
||||||
const byKey = entries.length > 0 ? await loadByIds(context, entries, 'own') : new Map<string, IMyRequest>();
|
const byKey =
|
||||||
|
entries.length > 0
|
||||||
|
? await traced('loadByIds:own(searchOwnRequests)', () => loadByIds(context, entries, 'own'))
|
||||||
|
: new Map<string, IMyRequest>();
|
||||||
// Порядок задаёт релевантность поиска, а не порядок ответов REST.
|
// Порядок задаёт релевантность поиска, а не порядок ответов REST.
|
||||||
const rows = entries.map((e) => byKey.get(requestKey(e.target.listId, e.id))).filter((r): r is IMyRequest => !!r);
|
const rows = entries.map((e) => byKey.get(requestKey(e.target.listId, e.id))).filter((r): r is IMyRequest => !!r);
|
||||||
|
|
||||||
@@ -1070,7 +1110,8 @@ export async function getOwnRequestsByStatus(
|
|||||||
if (text) clauses.push(text);
|
if (text) clauses.push(text);
|
||||||
const kql = clauses.join(' AND ');
|
const kql = clauses.join(' AND ');
|
||||||
|
|
||||||
const results = await sp.search({
|
const results = await traced('search:getOwnRequestsByStatus', () =>
|
||||||
|
sp.search({
|
||||||
Querytext: kql,
|
Querytext: kql,
|
||||||
RowLimit: PAGE_SIZE,
|
RowLimit: PAGE_SIZE,
|
||||||
StartRow: (page - 1) * PAGE_SIZE,
|
StartRow: (page - 1) * PAGE_SIZE,
|
||||||
@@ -1078,11 +1119,15 @@ export async function getOwnRequestsByStatus(
|
|||||||
TrimDuplicates: false,
|
TrimDuplicates: false,
|
||||||
// Свежие сверху — как в searchOwnRequests.
|
// Свежие сверху — как в searchOwnRequests.
|
||||||
SortList: [{ Property: 'Created', Direction: 1 }],
|
SortList: [{ Property: 'Created', Direction: 1 }],
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const totalRows = results.TotalRows || 0;
|
const totalRows = results.TotalRows || 0;
|
||||||
const entries = hitsToEntries(results, types);
|
const entries = hitsToEntries(results, types);
|
||||||
const byKey = entries.length > 0 ? await loadByIds(context, entries, 'own') : new Map<string, IMyRequest>();
|
const byKey =
|
||||||
|
entries.length > 0
|
||||||
|
? await traced('loadByIds:own(getOwnRequestsByStatus)', () => loadByIds(context, entries, 'own'))
|
||||||
|
: new Map<string, IMyRequest>();
|
||||||
const searchRows = entries.map((e) => byKey.get(requestKey(e.target.listId, e.id))).filter((r): r is IMyRequest => !!r);
|
const searchRows = entries.map((e) => byKey.get(requestKey(e.target.listId, e.id))).filter((r): r is IMyRequest => !!r);
|
||||||
|
|
||||||
if (page !== 1 || query.trim()) return { rows: searchRows, totalRows };
|
if (page !== 1 || query.trim()) return { rows: searchRows, totalRows };
|
||||||
@@ -1090,7 +1135,13 @@ export async function getOwnRequestsByStatus(
|
|||||||
// Довесок по недавним изменениям — только на первой странице и только без
|
// Довесок по недавним изменениям — только на первой странице и только без
|
||||||
// текстового поиска (тот же принцип, что в getOwnRequests): заявку могли
|
// текстового поиска (тот же принцип, что в getOwnRequests): заявку могли
|
||||||
// вернуть на доработку только что, индекс мог не успеть её подхватить.
|
// вернуть на доработку только что, индекс мог не успеть её подхватить.
|
||||||
const freshRows = (await Promise.all(types.map((t) => getRecentlyChangedOwnRowsForType(context, t))))
|
const freshRows = (
|
||||||
|
await Promise.all(
|
||||||
|
types.map((t) =>
|
||||||
|
traced(`getRecentlyChangedOwnRowsForType:${t.name}`, () => getRecentlyChangedOwnRowsForType(context, t))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
.reduce((acc, r) => acc.concat(r), [] as IMyRequest[])
|
.reduce((acc, r) => acc.concat(r), [] as IMyRequest[])
|
||||||
.filter((r) => r.status === status);
|
.filter((r) => r.status === status);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Замер времени отдельных этапов загрузки «Моих заявок» — что именно тормозит
|
||||||
|
* (индекс согласований, свои заявки по типу, конкретная партиция, Search),
|
||||||
|
* а не просто «страница долго грузится».
|
||||||
|
*
|
||||||
|
* Включение/выключение БЕЗ пересборки — из консоли браузера:
|
||||||
|
* window.rpPerfTrace = true // писать тайминги
|
||||||
|
* window.rpPerfTrace = false // выключить (по умолчанию)
|
||||||
|
*/
|
||||||
|
function enabled(): boolean {
|
||||||
|
return !!(window as unknown as { rpPerfTrace?: boolean }).rpPerfTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFIX = '[Перф]';
|
||||||
|
|
||||||
|
/** Оборачивает async-шаг замером времени. Без включённой трассировки — просто вызывает fn(). */
|
||||||
|
export async function traced<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
||||||
|
if (!enabled()) return fn();
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
console.log(`${PREFIX} ${label}: ${(performance.now() - start).toFixed(0)} мс`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,8 +14,12 @@ import { IRequestType } from '../../shared/approval';
|
|||||||
* window.rpSearchTrace = true // следить за всеми типами
|
* window.rpSearchTrace = true // следить за всеми типами
|
||||||
* window.rpSearchTrace = false // выключить
|
* window.rpSearchTrace = false // выключить
|
||||||
* Значение сверяется с именем типа и с pageUrl (ключом формы), регистр не важен.
|
* Значение сверяется с именем типа и с pageUrl (ключом формы), регистр не важен.
|
||||||
|
*
|
||||||
|
* По умолчанию выключена (false) — включённый DEFAULT_TRACE был точечной
|
||||||
|
* настройкой на время разбора конкретного бага с партициями «Обходного листа»
|
||||||
|
* и не должен постоянно писать в консоль всем остальным.
|
||||||
*/
|
*/
|
||||||
const DEFAULT_TRACE: string | boolean = 'обходной';
|
const DEFAULT_TRACE: string | boolean = false;
|
||||||
|
|
||||||
const PREFIX = '[Поиск]';
|
const PREFIX = '[Поиск]';
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
getPlannedMatrix,
|
getPlannedMatrix,
|
||||||
getCurrentListPartition,
|
resolveWriteListId,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
@@ -197,14 +197,7 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
|
|||||||
// списка и ставит в очередь создание следующей партиции при достижении —
|
// списка и ставит в очередь создание следующей партиции при достижении —
|
||||||
// без него архивация никогда не сработает. Сбой запроса не должен
|
// без него архивация никогда не сработает. Сбой запроса не должен
|
||||||
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
||||||
let writeListId = editListId;
|
const writeListId = await resolveWriteListId(context, FORM_ID, rtId, requestId, editListId);
|
||||||
if (!requestId) {
|
|
||||||
try {
|
|
||||||
writeListId = (await getCurrentListPartition(context, rtId)).listId;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { id, code } = await saveClearanceRequest(
|
const { id, code } = await saveClearanceRequest(
|
||||||
requestId,
|
requestId,
|
||||||
rtId,
|
rtId,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
getPlannedMatrix,
|
getPlannedMatrix,
|
||||||
getCurrentListPartition,
|
resolveWriteListId,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
@@ -194,14 +194,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
|||||||
// списка и ставит в очередь создание следующей партиции при достижении —
|
// списка и ставит в очередь создание следующей партиции при достижении —
|
||||||
// без него архивация никогда не сработает. Сбой запроса не должен
|
// без него архивация никогда не сработает. Сбой запроса не должен
|
||||||
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
||||||
let writeListId = editListId;
|
const writeListId = await resolveWriteListId(context, FORM_ID, rtId, requestId, editListId);
|
||||||
if (!requestId) {
|
|
||||||
try {
|
|
||||||
writeListId = (await getCurrentListPartition(context, rtId)).listId;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { id, code } = await saveHireRequest(
|
const { id, code } = await saveHireRequest(
|
||||||
requestId,
|
requestId,
|
||||||
rtId,
|
rtId,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
getPlannedMatrix,
|
getPlannedMatrix,
|
||||||
getCurrentListPartition,
|
resolveWriteListId,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
@@ -168,14 +168,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
|||||||
// списка и ставит в очередь создание следующей партиции при достижении —
|
// списка и ставит в очередь создание следующей партиции при достижении —
|
||||||
// без него архивация никогда не сработает. Сбой запроса не должен
|
// без него архивация никогда не сработает. Сбой запроса не должен
|
||||||
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
||||||
let writeListId = editListId;
|
const writeListId = await resolveWriteListId(context, FORM_ID, rtId, requestId, editListId);
|
||||||
if (!requestId) {
|
|
||||||
try {
|
|
||||||
writeListId = (await getCurrentListPartition(context, rtId)).listId;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { id, code } = await saveSelectionRequest(
|
const { id, code } = await saveSelectionRequest(
|
||||||
requestId,
|
requestId,
|
||||||
rtId,
|
rtId,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
getPlannedMatrix,
|
getPlannedMatrix,
|
||||||
getCurrentListPartition,
|
resolveWriteListId,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
@@ -233,14 +233,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
// списка и ставит в очередь создание следующей партиции при достижении —
|
// списка и ставит в очередь создание следующей партиции при достижении —
|
||||||
// без него архивация никогда не сработает. Сбой запроса не должен
|
// без него архивация никогда не сработает. Сбой запроса не должен
|
||||||
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
||||||
let writeListId = editListId;
|
const writeListId = await resolveWriteListId(context, FORM_ID, rtId, requestId, editListId);
|
||||||
if (!requestId) {
|
|
||||||
try {
|
|
||||||
writeListId = (await getCurrentListPartition(context, rtId)).listId;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id, code } = await saveTicketRequest(
|
const { id, code } = await saveTicketRequest(
|
||||||
requestId,
|
requestId,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
IApproverToWrite,
|
IApproverToWrite,
|
||||||
APPROVAL_PHASE,
|
APPROVAL_PHASE,
|
||||||
findUnassignedStep,
|
findUnassignedStep,
|
||||||
getCurrentListPartition
|
resolveWriteListId
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import {
|
import {
|
||||||
uploadAttachments,
|
uploadAttachments,
|
||||||
@@ -206,14 +206,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Новая заявка — резолвим ТЕКУЩУЮ партицию через approval-route (заодно
|
// Новая заявка — резолвим ТЕКУЩУЮ партицию через approval-route (заодно
|
||||||
// выполняется проверка порога и создание следующей архивной партиции при
|
// выполняется проверка порога и создание следующей архивной партиции при
|
||||||
// необходимости); при редактировании остаёмся в партиции самой заявки.
|
// необходимости); при редактировании остаёмся в партиции самой заявки.
|
||||||
let writeListId = editListId;
|
const writeListId = await resolveWriteListId(context, FORM_ID, rtId, requestId, editListId);
|
||||||
if (!requestId) {
|
|
||||||
try {
|
|
||||||
writeListId = (await getCurrentListPartition(context, rtId)).listId;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { id, code } = await saveTmcTransferRequest(
|
const { id, code } = await saveTmcTransferRequest(
|
||||||
requestId,
|
requestId,
|
||||||
rtId,
|
rtId,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
isEditableStatus,
|
isEditableStatus,
|
||||||
IApproverToWrite,
|
IApproverToWrite,
|
||||||
findUnassignedStep,
|
findUnassignedStep,
|
||||||
getCurrentListPartition
|
resolveWriteListId
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import {
|
import {
|
||||||
@@ -180,14 +180,7 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Новая заявка пишется в ТЕКУЩУЮ партицию — запрашиваем её у approval-route
|
// Новая заявка пишется в ТЕКУЩУЮ партицию — запрашиваем её у approval-route
|
||||||
// API непосредственно перед записью (не кеш из onInit): именно этот вызов
|
// API непосредственно перед записью (не кеш из onInit): именно этот вызов
|
||||||
// проверяет порог элементов и запускает архивацию следующей партиции.
|
// проверяет порог элементов и запускает архивацию следующей партиции.
|
||||||
let writeListId = editListId;
|
const writeListId = await resolveWriteListId(context, FORM_ID, rtId, requestId, editListId);
|
||||||
if (!requestId) {
|
|
||||||
try {
|
|
||||||
writeListId = (await getCurrentListPartition(context, rtId)).listId;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { id, code } = await saveUnpaidLeaveRequest(
|
const { id, code } = await saveUnpaidLeaveRequest(
|
||||||
requestId,
|
requestId,
|
||||||
rtId,
|
rtId,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
getPlannedMatrix,
|
getPlannedMatrix,
|
||||||
getCurrentListPartition,
|
resolveWriteListId,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
@@ -175,20 +175,9 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
throw new Error('Тип заявки не определён — обновите страницу и повторите.');
|
throw new Error('Тип заявки не определён — обновите страницу и повторите.');
|
||||||
}
|
}
|
||||||
// requestId уже задан → редактируем существующую заявку, пишем в ту же
|
// requestId уже задан → редактируем существующую заявку, пишем в ту же
|
||||||
// партицию (editListId). Иначе — создаём новую, и партицию для записи
|
// партицию (editListId); иначе резолвится ТЕКУЩАЯ (см. resolveWriteListId —
|
||||||
// спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit):
|
// именно этот запрос проверяет порог элементов и запускает архивацию).
|
||||||
// это единственный запрос, который проверяет порог элементов текущего
|
const writeListId = await resolveWriteListId(context, FORM_ID, rtId, requestId, editListId);
|
||||||
// списка и ставит в очередь создание следующей партиции при достижении —
|
|
||||||
// без него архивация никогда не сработает. Сбой запроса не должен
|
|
||||||
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
|
||||||
let writeListId = editListId;
|
|
||||||
if (!requestId) {
|
|
||||||
try {
|
|
||||||
writeListId = (await getCurrentListPartition(context, rtId)).listId;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { id, code } = await saveVacationRequest(
|
const { id, code } = await saveVacationRequest(
|
||||||
requestId,
|
requestId,
|
||||||
rtId,
|
rtId,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
useMatrixEditor,
|
useMatrixEditor,
|
||||||
getPlannedMatrix,
|
getPlannedMatrix,
|
||||||
getCurrentListPartition,
|
resolveWriteListId,
|
||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
REQUEST_STATUS,
|
REQUEST_STATUS,
|
||||||
@@ -218,14 +218,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
|||||||
// списка и ставит в очередь создание следующей партиции при достижении —
|
// списка и ставит в очередь создание следующей партиции при достижении —
|
||||||
// без него архивация никогда не сработает. Сбой запроса не должен
|
// без него архивация никогда не сработает. Сбой запроса не должен
|
||||||
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
// блокировать подачу заявки — откатываемся на список по конвенции имён.
|
||||||
let writeListId = editListId;
|
const writeListId = await resolveWriteListId(context, FORM_ID, rtId, requestId, editListId);
|
||||||
if (!requestId) {
|
|
||||||
try {
|
|
||||||
writeListId = (await getCurrentListPartition(context, rtId)).listId;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { id, code } = await saveWorkplaceRequest(
|
const { id, code } = await saveWorkplaceRequest(
|
||||||
requestId,
|
requestId,
|
||||||
rtId,
|
rtId,
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ export {
|
|||||||
currentPartitionForPageUrl,
|
currentPartitionForPageUrl,
|
||||||
partitionForPageUrlAndListId,
|
partitionForPageUrlAndListId,
|
||||||
isConfirmedStaleListId,
|
isConfirmedStaleListId,
|
||||||
|
registerCurrentPartition,
|
||||||
} from './listPartitions';
|
} from './listPartitions';
|
||||||
|
export { resolveWriteListId } from './writeListId';
|
||||||
export { useRequestTypeId } from './useRequestTypeId';
|
export { useRequestTypeId } from './useRequestTypeId';
|
||||||
export type { IRequestTypeIdState } from './useRequestTypeId';
|
export type { IRequestTypeIdState } from './useRequestTypeId';
|
||||||
export { ApprovalListService } from './approvalList.service';
|
export { ApprovalListService } from './approvalList.service';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { getListPartitions, getRequestTypes } from './approvalRoute.service';
|
import { getListPartitions, getRequestTypes } from './approvalRoute.service';
|
||||||
import { IListPartition } from './models/IApprovalRouteApi';
|
import { ICurrentListPartition, IListPartition } from './models/IApprovalRouteApi';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Реестр партиций списков — единственное место, которое знает, куда ПИСАТЬ и где
|
* Реестр партиций списков — единственное место, которое знает, куда ПИСАТЬ и где
|
||||||
@@ -90,6 +90,45 @@ export function currentPartitionForPageUrl(pageUrl: string): IListPartition | un
|
|||||||
return id === undefined ? undefined : currentPartitionForTypeId(id);
|
return id === undefined ? undefined : currentPartitionForTypeId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вписывает в кеш партицию, которую approval-route ТОЛЬКО ЧТО вернул как текущую
|
||||||
|
* (см. getCurrentListPartition) — вызывать сразу после успешного ответа, ДО того
|
||||||
|
* как эта партиция понадобится дальше в том же сохранении (approvalListTitleForListId
|
||||||
|
* и т.п.), а не только при следующей перезагрузке страницы.
|
||||||
|
*
|
||||||
|
* Без этого партиция, созданная провижинингом ПОСЛЕ onInit (порог элементов
|
||||||
|
* достигнут прямо во время этого запроса), была бы неизвестна isConfirmedStaleListId,
|
||||||
|
* и попытка сохранить в неё же заявку — записав данные в новый список — падала бы
|
||||||
|
* следующим шагом с «список не найден в реестре партиций», хотя ссылка совершенно
|
||||||
|
* свежая и рабочая (см. форму пользователя: данные создались, ошибка была только
|
||||||
|
* в определении имени списка согласования).
|
||||||
|
*
|
||||||
|
* Тип, о котором реестр вообще ничего не знает (initListPartitions не ответил при
|
||||||
|
* старте — typeIdByPageUrl пуст для этого pageUrl), не трогаем: мержить партицию в
|
||||||
|
* несуществующую серию нечем, а откат на конвенцию имён и так работает без реестра.
|
||||||
|
*/
|
||||||
|
export function registerCurrentPartition(pageUrl: string, partition: ICurrentListPartition): void {
|
||||||
|
const id = typeIdByPageUrl.get(pageUrl);
|
||||||
|
if (id === undefined) return;
|
||||||
|
|
||||||
|
const lower = partition.listId.toLowerCase();
|
||||||
|
const merged: IListPartition[] = partitionsForTypeId(id)
|
||||||
|
.filter((p) => p.listId.toLowerCase() !== lower)
|
||||||
|
.map((p) => ({ ...p, isCurrent: false }));
|
||||||
|
|
||||||
|
merged.push({
|
||||||
|
number: partition.number,
|
||||||
|
siteUrl: partition.siteUrl,
|
||||||
|
listId: partition.listId,
|
||||||
|
title: partition.title,
|
||||||
|
isCurrent: true,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
merged.sort((a, b) => a.number - b.number);
|
||||||
|
|
||||||
|
partitionsByTypeId.set(id, merged);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Конкретная партиция по её listId — нужна, чтобы открыть/сохранить АРХИВНУЮ
|
* Конкретная партиция по её listId — нужна, чтобы открыть/сохранить АРХИВНУЮ
|
||||||
* заявку: тогда известен не номер, а сам listId (пришёл из строки поиска, см.
|
* заявку: тогда известен не номер, а сам listId (пришёл из строки поиска, см.
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { RequestTypeId } from '../../models/types';
|
||||||
|
import { getCurrentListPartition } from './approvalRoute.service';
|
||||||
|
import { registerCurrentPartition } from './listPartitions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Партиция, в которую фактически писать при сохранении заявки.
|
||||||
|
*
|
||||||
|
* existingId задан → редактируем существующую заявку, пишем в ту же партицию
|
||||||
|
* (editListId), запись новых партиций тут ни при чём.
|
||||||
|
*
|
||||||
|
* existingId не задан → создаём новую заявку, и партицию для записи спрашиваем
|
||||||
|
* у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit) — именно этот вызов
|
||||||
|
* проверяет порог элементов текущего списка и ставит в очередь создание
|
||||||
|
* следующей партиции при достижении; без него архивация никогда не сработает,
|
||||||
|
* сколько бы заявок ни создали.
|
||||||
|
*
|
||||||
|
* Свежерезолвленная партиция сразу вписывается в локальный кеш
|
||||||
|
* (registerCurrentPartition): если её создало провижинирование ПРЯМО ЭТИМ
|
||||||
|
* запросом (порог как раз достигнут), кеш из onInit о ней ещё не знает, и без
|
||||||
|
* этого следующий же шаг сохранения (обычно резолв имени списка согласования
|
||||||
|
* через approvalListTitleForListId) упал бы с «список не найден в реестре
|
||||||
|
* партиций», хотя данные уже успешно записались и ссылка совершенно рабочая —
|
||||||
|
* ровно так это и проявилось на форме «Обходной лист».
|
||||||
|
*
|
||||||
|
* Сбой запроса к approval-route не должен блокировать подачу заявки — откат на
|
||||||
|
* editListId (для новой заявки это undefined → список по конвенции имён,
|
||||||
|
* прежнее поведение до появления партиционирования).
|
||||||
|
*
|
||||||
|
* Раньше эта логика (try/catch + getCurrentListPartition + registerCurrentPartition)
|
||||||
|
* была скопирована в save() каждой из 8 форм — при первом же баге в ней пришлось
|
||||||
|
* чинить один и тот же код в 8 файлах. Вынесена сюда, чтобы чинить один раз.
|
||||||
|
*/
|
||||||
|
export async function resolveWriteListId(
|
||||||
|
context: WebPartContext,
|
||||||
|
formId: RequestTypeId,
|
||||||
|
requestTypeId: number,
|
||||||
|
existingId: number | undefined,
|
||||||
|
editListId: string | undefined
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (existingId) return editListId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const partition = await getCurrentListPartition(context, requestTypeId);
|
||||||
|
registerCurrentPartition(formId, partition);
|
||||||
|
return partition.listId;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e);
|
||||||
|
return editListId;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user