This commit is contained in:
@@ -16,7 +16,6 @@ import { effectiveUserEmail } from '../../shared/impersonation';
|
||||
import { IMyRequest } from './models/IMyRequest';
|
||||
import { IMyRequestsTypeDebug } from './models/IMyRequestsDebug';
|
||||
import { traceLinkedTypes, traceResultRows, traceSearchHits } from './searchTrace';
|
||||
import { traced } from './perfTrace';
|
||||
|
||||
// Список данных «... — данные» → список согласования «... — согласование»
|
||||
// (конвенция именования; см. forms/*/provisioning.json и forms/*/approval/provisioning.json).
|
||||
@@ -203,9 +202,7 @@ async function buildPendingIndex(
|
||||
// полноценный Search, который эти вызовы как раз заменяют.
|
||||
const resolved = await Promise.all(
|
||||
flat.map(({ type, target }) =>
|
||||
traced(`resolveApprovalSearchTarget:${type.name}#${target.listId}`, () =>
|
||||
resolveApprovalSearchTarget(context, type, target)
|
||||
)
|
||||
resolveApprovalSearchTarget(context, type, target)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -228,24 +225,22 @@ async function buildPendingIndex(
|
||||
// поэтому идут параллельно, а не один за другим.
|
||||
const [hitsByTarget, recentByTarget] = await Promise.all([
|
||||
okEntries.length > 0
|
||||
? traced('search:approvalsAcrossAll', () => searchAcrossApprovalLists(context, okEntries, tokenEmail, statuses))
|
||||
? searchAcrossApprovalLists(context, okEntries, tokenEmail, statuses)
|
||||
: Promise.resolve(new Map<string, number[]>()),
|
||||
// Довесок нужен только режиму «чья очередь сейчас»: при текстовом поиске
|
||||
// важна полнота по архиву, а не мгновенная свежесть (см. scope='involved').
|
||||
okEntries.length > 0 && scope === 'myTurn'
|
||||
? traced('recentRowsAcrossAll', () => fetchRecentApprovalRows(context, okEntries, tokenEmail, statuses))
|
||||
? fetchRecentApprovalRows(context, okEntries, tokenEmail, statuses)
|
||||
: Promise.resolve(new Map<string, Array<Record<string, unknown>>>()),
|
||||
]);
|
||||
|
||||
const parts = await Promise.all(
|
||||
okEntries.map((entry) =>
|
||||
traced(`finalizeApprovalTarget:${entry.type.name}#${entry.target.listId}`, () =>
|
||||
finalizeApprovalTarget(
|
||||
context,
|
||||
entry,
|
||||
hitsByTarget.get(targetKey(entry)) ?? [],
|
||||
recentByTarget.get(targetKey(entry)) ?? []
|
||||
)
|
||||
finalizeApprovalTarget(
|
||||
context,
|
||||
entry,
|
||||
hitsByTarget.get(targetKey(entry)) ?? [],
|
||||
recentByTarget.get(targetKey(entry)) ?? []
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -343,15 +338,15 @@ export interface INavCounts {
|
||||
// своим запросом — но параллельно, и ровно тем же вызовом, что наполняет саму
|
||||
// вкладку: иначе счётчик и список могли бы разойтись.
|
||||
export async function getNavCounts(context: WebPartContext): Promise<INavCounts> {
|
||||
return traced('ИТОГО getNavCounts', () => getNavCountsInner(context));
|
||||
return getNavCountsInner(context);
|
||||
}
|
||||
|
||||
async function getNavCountsInner(context: WebPartContext): Promise<INavCounts> {
|
||||
const types = await getLinkedTypes(context);
|
||||
|
||||
const [turn, rework] = await Promise.all([
|
||||
traced('buildPendingIndex:myTurn', () => buildPendingIndex(context, 'myTurn', MY_TURN_STATUSES)),
|
||||
traced('getOwnRequestsByStatus:rework', () => getOwnRequestsByStatus(context, types, REQUEST_STATUS.returned, 1)),
|
||||
buildPendingIndex(context, 'myTurn', MY_TURN_STATUSES),
|
||||
getOwnRequestsByStatus(context, types, REQUEST_STATUS.returned, 1),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -419,7 +414,7 @@ async function resolveApprovalSearchTarget(
|
||||
|
||||
const dataTitle =
|
||||
target.title ??
|
||||
((await traced(`dataListTitle:${type.name}#${target.listId}`, () => list.select('Title')())).Title as string) ??
|
||||
((await list.select('Title')()).Title as string) ??
|
||||
'';
|
||||
if (!DATA_LIST_SUFFIX.test(dataTitle)) {
|
||||
return {
|
||||
@@ -432,9 +427,9 @@ async function resolveApprovalSearchTarget(
|
||||
const approvalListTitle = `${dataTitle.replace(DATA_LIST_SUFFIX, '')} — согласование`;
|
||||
const approvalList = sp.web.lists.getByTitle(approvalListTitle);
|
||||
|
||||
const rootFolder = (await traced(`approvalListUrl:${type.name}#${target.listId}`, () =>
|
||||
approvalList.select('RootFolder/ServerRelativeUrl').expand('RootFolder')()
|
||||
)) as { RootFolder?: { ServerRelativeUrl?: string } };
|
||||
const rootFolder = (await approvalList.select('RootFolder/ServerRelativeUrl').expand('RootFolder')()) as {
|
||||
RootFolder?: { ServerRelativeUrl?: string };
|
||||
};
|
||||
const relUrl = rootFolder.RootFolder?.ServerRelativeUrl;
|
||||
if (!relUrl) {
|
||||
return { kind: 'error', type, target, message: `Список согласования «${approvalListTitle}» не найден` };
|
||||
@@ -583,7 +578,7 @@ async function fetchRecentApprovalRows(
|
||||
// SharePoint REST — поэтому резолвим email в числовой Id (как и везде в
|
||||
// проекте) и фильтруем по TansuSiteApproverId.
|
||||
const claimsLogin = tokenEmail.indexOf('|') >= 0 ? tokenEmail : `i:0#.f|membership|${tokenEmail}`;
|
||||
const approver = await traced(`ensureUser:${siteUrl}`, () => sp.web.ensureUser(claimsLogin));
|
||||
const approver = await sp.web.ensureUser(claimsLogin);
|
||||
const filter = `TansuSiteApproverId eq ${approver.Id} and Modified ge '${cutoffIso}'${statusFilter}`;
|
||||
|
||||
const [batchedSP, execute] = sp.batched();
|
||||
@@ -609,7 +604,7 @@ async function fetchRecentApprovalRows(
|
||||
})
|
||||
);
|
||||
|
||||
await traced(`recentRowsBatch:${siteUrl} (партиций: ${siteEntries.length})`, execute);
|
||||
await execute();
|
||||
await Promise.all(pending);
|
||||
} catch (e) {
|
||||
console.error(`[Текущие заявки] Довесок по свежести для ${siteUrl} не сработал:`, (e as Error).message);
|
||||
@@ -662,12 +657,10 @@ async function finalizeApprovalTarget(
|
||||
// Id режем на пачки — тот же предел длины URL REST $filter, что и в
|
||||
// loadByIds (см. REST_ID_FILTER_CHUNK).
|
||||
chunk(searchHitIds, REST_ID_FILTER_CHUNK).map((idsChunk) =>
|
||||
traced(`approvalRowsRest:${type.name}#${target.listId}`, () =>
|
||||
approvalList.items
|
||||
.filter(idsChunk.map((id) => `Id eq ${id}`).join(' or '))
|
||||
.select('Id', 'TicketId', 'ApprovalStatus')
|
||||
.top(idsChunk.length)()
|
||||
)
|
||||
approvalList.items
|
||||
.filter(idsChunk.map((id) => `Id eq ${id}`).join(' or '))
|
||||
.select('Id', 'TicketId', 'ApprovalStatus')
|
||||
.top(idsChunk.length)()
|
||||
)
|
||||
)
|
||||
).reduce((all, r) => all.concat(r as Array<Record<string, unknown>>), [] as Array<Record<string, unknown>>);
|
||||
@@ -702,12 +695,10 @@ async function finalizeApprovalTarget(
|
||||
// Заголовок/статус/дата заявки — отдельным запросом к списку ДАННЫХ (см.
|
||||
// комментарий у функции: через Lookup-expand это не отдаётся).
|
||||
const ticketFilter = ticketIds.map((id) => `Id eq ${id}`).join(' or ');
|
||||
const ticketRows = (await traced(`ticketRowsRest:${type.name}#${target.listId}`, () =>
|
||||
list.items
|
||||
.filter(ticketFilter)
|
||||
.select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created')
|
||||
.top(ticketIds.length)()
|
||||
)) as Array<Record<string, unknown>>;
|
||||
const ticketRows = (await list.items
|
||||
.filter(ticketFilter)
|
||||
.select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created')
|
||||
.top(ticketIds.length)()) as Array<Record<string, unknown>>;
|
||||
|
||||
debug.rows = ticketRows.map((r) => ({
|
||||
...toMyRequest(r, undefined, type, 'approval', target),
|
||||
@@ -831,16 +822,14 @@ async function fetchAllSearchRows(
|
||||
let startRow = 0;
|
||||
|
||||
for (;;) {
|
||||
const results = await traced(`search:${label}#${startRow}`, () =>
|
||||
sp.search({
|
||||
Querytext: kql,
|
||||
RowLimit: SEARCH_PAGE_SIZE,
|
||||
StartRow: startRow,
|
||||
SelectProperties: selectProperties,
|
||||
TrimDuplicates: false,
|
||||
...(sortList ? { SortList: sortList } : {}),
|
||||
})
|
||||
);
|
||||
const results = await sp.search({
|
||||
Querytext: kql,
|
||||
RowLimit: SEARCH_PAGE_SIZE,
|
||||
StartRow: startRow,
|
||||
SelectProperties: selectProperties,
|
||||
TrimDuplicates: false,
|
||||
...(sortList ? { SortList: sortList } : {}),
|
||||
});
|
||||
|
||||
const page = results.PrimarySearchResults as Array<Record<string, unknown>>;
|
||||
rows.push(...page);
|
||||
@@ -1080,13 +1069,11 @@ async function loadByIds(
|
||||
|
||||
await Promise.all(
|
||||
chunk(ids, REST_ID_FILTER_CHUNK).map(async (idsChunk) => {
|
||||
const rows = (await traced(`loadByIds:${kind}:${type.name}#${target.listId}`, () =>
|
||||
list.items
|
||||
.filter(idsChunk.map((id) => `Id eq ${id}`).join(' or '))
|
||||
.select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.expand('Author')
|
||||
.top(idsChunk.length)()
|
||||
)) as Array<Record<string, unknown>>;
|
||||
const rows = (await list.items
|
||||
.filter(idsChunk.map((id) => `Id eq ${id}`).join(' or '))
|
||||
.select('Id', 'RequestUniqueCode', 'Title', 'RequestStatus', 'Created', 'Author/EMail', 'Author/Title')
|
||||
.expand('Author')
|
||||
.top(idsChunk.length)()) as Array<Record<string, unknown>>;
|
||||
rows.forEach((r) => {
|
||||
const author = r.Author as { EMail?: string; Title?: string } | undefined;
|
||||
byKey.set(
|
||||
@@ -1205,13 +1192,13 @@ export async function getOwnRequests(
|
||||
): Promise<IOwnRequestsPage> {
|
||||
if (types.length === 0) return { rows: [], totalRows: 0 };
|
||||
|
||||
const searchPage = await traced('searchOwnRequests', () => searchOwnRequests(context, types, '', page));
|
||||
const searchPage = await searchOwnRequests(context, types, '', page);
|
||||
|
||||
if (page !== 1) return searchPage;
|
||||
|
||||
const freshRows = (
|
||||
await Promise.all(
|
||||
types.map((t) => traced(`getFreshOwnRowsForType:${t.name}`, () => getFreshOwnRowsForType(context, t)))
|
||||
types.map((t) => getFreshOwnRowsForType(context, t))
|
||||
)
|
||||
).reduce((all, r) => all.concat(r), [] as IMyRequest[]);
|
||||
|
||||
@@ -1248,23 +1235,21 @@ export async function searchOwnRequests(
|
||||
if (text) clauses.push(text);
|
||||
const kql = clauses.join(' AND ');
|
||||
|
||||
const results = await traced('search:searchOwnRequests', () =>
|
||||
sp.search({
|
||||
Querytext: kql,
|
||||
RowLimit: PAGE_SIZE,
|
||||
StartRow: (page - 1) * PAGE_SIZE,
|
||||
SelectProperties: ['ListItemID', 'Title', 'Path'],
|
||||
TrimDuplicates: false,
|
||||
// Свежие сверху (по умолчанию Search сортирует по релевантности, не по дате).
|
||||
SortList: [{ Property: 'Created', Direction: 1 }],
|
||||
})
|
||||
);
|
||||
const results = await sp.search({
|
||||
Querytext: kql,
|
||||
RowLimit: PAGE_SIZE,
|
||||
StartRow: (page - 1) * PAGE_SIZE,
|
||||
SelectProperties: ['ListItemID', 'Title', 'Path'],
|
||||
TrimDuplicates: false,
|
||||
// Свежие сверху (по умолчанию Search сортирует по релевантности, не по дате).
|
||||
SortList: [{ Property: 'Created', Direction: 1 }],
|
||||
});
|
||||
|
||||
const totalRows = results.TotalRows || 0;
|
||||
const entries = hitsToEntries(results, types);
|
||||
const byKey =
|
||||
entries.length > 0
|
||||
? await traced('loadByIds:own(searchOwnRequests)', () => loadByIds(context, entries, 'own'))
|
||||
? await loadByIds(context, entries, 'own')
|
||||
: new Map<string, IMyRequest>();
|
||||
// Порядок задаёт релевантность поиска, а не порядок ответов REST.
|
||||
const rows = entries.map((e) => byKey.get(requestKey(e.target.listId, e.id))).filter((r): r is IMyRequest => !!r);
|
||||
@@ -1299,23 +1284,21 @@ export async function getOwnRequestsByStatus(
|
||||
if (text) clauses.push(text);
|
||||
const kql = clauses.join(' AND ');
|
||||
|
||||
const results = await traced('search:getOwnRequestsByStatus', () =>
|
||||
sp.search({
|
||||
Querytext: kql,
|
||||
RowLimit: PAGE_SIZE,
|
||||
StartRow: (page - 1) * PAGE_SIZE,
|
||||
SelectProperties: ['ListItemID', 'Title', 'Path'],
|
||||
TrimDuplicates: false,
|
||||
// Свежие сверху — как в searchOwnRequests.
|
||||
SortList: [{ Property: 'Created', Direction: 1 }],
|
||||
})
|
||||
);
|
||||
const results = await sp.search({
|
||||
Querytext: kql,
|
||||
RowLimit: PAGE_SIZE,
|
||||
StartRow: (page - 1) * PAGE_SIZE,
|
||||
SelectProperties: ['ListItemID', 'Title', 'Path'],
|
||||
TrimDuplicates: false,
|
||||
// Свежие сверху — как в searchOwnRequests.
|
||||
SortList: [{ Property: 'Created', Direction: 1 }],
|
||||
});
|
||||
|
||||
const totalRows = results.TotalRows || 0;
|
||||
const entries = hitsToEntries(results, types);
|
||||
const byKey =
|
||||
entries.length > 0
|
||||
? await traced('loadByIds:own(getOwnRequestsByStatus)', () => loadByIds(context, entries, 'own'))
|
||||
? await 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);
|
||||
|
||||
@@ -1326,9 +1309,7 @@ export async function getOwnRequestsByStatus(
|
||||
// вернуть на доработку только что, индекс мог не успеть её подхватить.
|
||||
const freshRows = (
|
||||
await Promise.all(
|
||||
types.map((t) =>
|
||||
traced(`getRecentlyChangedOwnRowsForType:${t.name}`, () => getRecentlyChangedOwnRowsForType(context, t))
|
||||
)
|
||||
types.map((t) => getRecentlyChangedOwnRowsForType(context, t))
|
||||
)
|
||||
)
|
||||
.reduce((acc, r) => acc.concat(r), [] as IMyRequest[])
|
||||
|
||||
Reference in New Issue
Block a user