дозагрузка свежих данных

This commit is contained in:
gorbenko.pavel
2026-08-05 13:53:54 +05:00
parent d8737a15e3
commit 8f7d81d2b6
10 changed files with 648 additions and 119 deletions
@@ -625,6 +625,101 @@ export async function filterPendingByQuery(
return freshMatches.concat(olderMatches);
}
// ---------------------------------------------------------------------------
// Вкладка «Поиск»: ищет сразу по двум категориям — «Мои заявки» (я автор) и
// «где я участвовал как согласующий» (involved, независимо от того, дошла ли
// уже очередь) — и показывает их одним списком, с колонками Автор и Статус
// одновременно (в отличие от «Мои заявки»/«На согласовании», где по смыслу
// раздела одно из двух не нужно).
//
// Пагинация — клиентская поверх объединённого результата: «Мои заявки» уже
// умеет по-настоящему постранично через Search, но «где участвовал» — нет
// (см. filterPendingByQuery — там честного $skip/$top не сделать), так что
// единой серверной пагинации на двух источниках сразу не построить. Для
// сценария «искал что-то один раз» это приемлемо.
export interface ISearchPage {
rows: IMyRequest[];
totalRows: number;
}
// Как getOwnRequests, но без $skip/$top — сразу весь найденный срез (до
// KQL_ID_LIMIT), с тем же довеском свежих (< 10 мин) заявок через REST.
async function searchOwnAll(
context: WebPartContext,
types: IRequestType[],
query: string
): Promise<IMyRequest[]> {
if (types.length === 0) return [];
const sp = spfi(types[0].siteUrl as string).using(SPFx(context));
const claimsLogin = `i:0#.f|membership|${currentUserEmail(context)}`;
const clauses = [
`(${types.map((t) => `Path:"${listAbsUrl(t)}*"`).join(' OR ')})`,
`AuthorOWSUSER:"${claimsLogin}"`,
];
const text = freeTextClause(query);
if (text) clauses.push(text);
const kql = clauses.join(' AND ');
const results = await sp.search({
Querytext: kql,
RowLimit: KQL_ID_LIMIT,
SelectProperties: ['ListItemID', 'Title', 'Path'],
TrimDuplicates: false,
});
console.log(`[Текущие заявки] Search «Поиск» (мои заявки): KQL = ${kql}, найдено: ${results.TotalRows}`);
console.table(results.PrimarySearchResults);
const entries = hitsToEntries(results, types);
const byKey = entries.length > 0 ? await loadByIds(context, entries, 'own') : new Map<string, IMyRequest>();
const searchRows = entries.map((e) => byKey.get(`${e.type.id}-${e.id}`)).filter((r): r is IMyRequest => !!r);
const freshRows = (await Promise.all(types.map((t) => getFreshOwnRowsForType(context, t, query)))).reduce(
(all, r) => all.concat(r),
[] as IMyRequest[]
);
const searchKeys = new Set(searchRows.map((r) => `${r.requestTypeId}-${r.id}`));
const onlyFresh = freshRows.filter((r) => !searchKeys.has(`${r.requestTypeId}-${r.id}`));
return onlyFresh.concat(searchRows);
}
export async function searchMyRequests(
context: WebPartContext,
types: IRequestType[],
query: string,
page: number
): Promise<ISearchPage> {
const q = query.trim();
if (!q) return { rows: [], totalRows: 0 };
const [ownRows, involvedIdx] = await Promise.all([
searchOwnAll(context, types, q),
getPendingApprovalIndex(context, 'involved'),
]);
const involvedFiltered = await filterPendingByQuery(context, involvedIdx.types, involvedIdx.rows, q);
// Заявка может оказаться и своей, и involved одновременно (сам себе
// согласующий) — оставляем как 'own', не дублируем строку.
const ownKeys = new Set(ownRows.map((r) => `${r.requestTypeId}-${r.id}`));
const involvedOnly = involvedFiltered.filter((r) => !ownKeys.has(`${r.requestTypeId}-${r.id}`));
const merged = ownRows.concat(involvedOnly).sort((a, b) => b.created.localeCompare(a.created));
const totalRows = merged.length;
const pageRows = merged.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
// Автор нужен для 'approval'-строк текущей страницы — 'own' уже пришли с
// автором (loadByIds/getFreshOwnRowsForType сами дочитывают Author).
const needAuthor = pageRows.filter((r) => r.kind === 'approval');
if (needAuthor.length > 0) {
await hydratePendingAuthors(context, involvedIdx.types, needAuthor);
}
return { rows: pageRows, totalRows };
}
function toMyRequest(
r: Record<string, unknown>,
author: { EMail?: string; Title?: string } | undefined,