убрал лишние серчи и ресты
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
getPendingApproval,
|
getPendingApproval,
|
||||||
getPendingApprovalIndex,
|
getPendingApprovalIndex,
|
||||||
hydratePendingAuthors,
|
hydratePendingAuthors,
|
||||||
|
searchOwnRequests,
|
||||||
} from './myRequests.service';
|
} from './myRequests.service';
|
||||||
import { IMyRequest } from './models/IMyRequest';
|
import { IMyRequest } from './models/IMyRequest';
|
||||||
import { REQUEST_STATUS, IRequestType } from '../../shared/approval';
|
import { REQUEST_STATUS, IRequestType } from '../../shared/approval';
|
||||||
@@ -116,12 +117,19 @@ const Table: React.FC<{
|
|||||||
|
|
||||||
// Раздел с таблицей заявок: поиск по всем полям + вывод постранично.
|
// Раздел с таблицей заявок: поиск по всем полям + вывод постранично.
|
||||||
//
|
//
|
||||||
// 'own' — Search сразу с пагинацией (RowLimit/StartRow): автор = текущий
|
// Движок выбирается по наличию текста в поиске, а не по разделу: БЕЗ запроса —
|
||||||
// пользователь + текст запроса по всем полям.
|
// всегда обычный REST (мгновенно свежий, без задержки индексации), С запросом —
|
||||||
|
// всегда Search (полнотекстовый, но переиндексируется с задержкой). Раньше
|
||||||
|
// поиск подмешивал REST для «свежих» строк поверх Search — из-за этого
|
||||||
|
// результат мог быть неустойчивым (заявка то находилась, то нет, в зависимости
|
||||||
|
// от того, попала ли она в окно подстраховки); эту гибридную логику убрали.
|
||||||
//
|
//
|
||||||
// 'approvals' без поиска — обычный REST, отфильтрованный по ApproverId(+Status),
|
// 'own' без поиска — getOwnRequests (REST по AuthorId, страница режется в
|
||||||
// страница нарезается на СЕРВЕРЕ (getPendingApproval сам берёт нужный срез
|
// памяти). 'own' с поиском — searchOwnRequests (Search, RowLimit/StartRow).
|
||||||
// индекса и дочитывает автора только для него).
|
//
|
||||||
|
// 'approvals' без поиска — getPendingApproval, REST по ApproverId(+Status),
|
||||||
|
// страница нарезается на СЕРВЕРЕ (сам берёт нужный срез индекса и дочитывает
|
||||||
|
// автора только для него).
|
||||||
// 'approvals' с поиском — область расширяется до «где пользователь вообще есть
|
// 'approvals' с поиском — область расширяется до «где пользователь вообще есть
|
||||||
// в маршруте» (getPendingApprovalIndex, ПОЛНЫЙ список без автора — постранично
|
// в маршруте» (getPendingApprovalIndex, ПОЛНЫЙ список без автора — постранично
|
||||||
// его тут не взять, т.к. фильтрация текстом идёт уже после), затем текстовый
|
// его тут не взять, т.к. фильтрация текстом идёт уже после), затем текстовый
|
||||||
@@ -168,7 +176,10 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
|||||||
const load = async (): Promise<void> => {
|
const load = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (kind === 'own') {
|
if (kind === 'own') {
|
||||||
const r = await getOwnRequests(context, types, query, page);
|
const hasOwnQuery = query.trim().length > 0;
|
||||||
|
const r = hasOwnQuery
|
||||||
|
? await searchOwnRequests(context, types, query, page)
|
||||||
|
: await getOwnRequests(context, types, page);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setRows(r.rows);
|
setRows(r.rows);
|
||||||
setTotalRows(r.totalRows);
|
setTotalRows(r.totalRows);
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ const DATA_LIST_SUFFIX = /\s*—\s*данные\s*$/;
|
|||||||
|
|
||||||
// Свежие заявки/строки (созданы или изменены недавно) поисковый индекс может
|
// Свежие заявки/строки (созданы или изменены недавно) поисковый индекс может
|
||||||
// ещё не проиндексировать — такие добираем обычным REST-ом напрямую, в обход
|
// ещё не проиндексировать — такие добираем обычным REST-ом напрямую, в обход
|
||||||
// Search, и подмешиваем к результату (см. getOwnRequests, indexApprovalsForType,
|
// Search, и подмешиваем к результату (см. indexApprovalsForType/myTurn,
|
||||||
// filterPendingByQuery).
|
// getFreshOwnRowsForType). Используется ТОЛЬКО в паре с Search (гибрид для
|
||||||
|
// таблиц без текстового запроса — там, где важны и масштаб, и мгновенная
|
||||||
|
// свежесть); при поиске по тексту эта подстраховка не применяется (см. RequestsTableView).
|
||||||
const RECENT_MS = 10 * 60 * 1000;
|
const RECENT_MS = 10 * 60 * 1000;
|
||||||
|
|
||||||
export function currentUserEmail(context: WebPartContext): string {
|
export function currentUserEmail(context: WebPartContext): string {
|
||||||
@@ -178,41 +180,42 @@ async function indexApprovalsForType(
|
|||||||
let stepRows: Array<Record<string, unknown>>;
|
let stepRows: Array<Record<string, unknown>>;
|
||||||
|
|
||||||
if (scope === 'myTurn') {
|
if (scope === 'myTurn') {
|
||||||
// 'myTurn' решает, надо ли действовать ПРЯМО СЕЙЧАС — Status должен быть
|
// 'myTurn' — гибрид для масштаба (список согласования может быть очень
|
||||||
// гарантированно свежим (индекс Search обновляется с задержкой и мог бы
|
// большим): Search находит кандидатов по managed property ApproverOWSUSER
|
||||||
// показать уже решённый шаг как ожидающий). Поэтому здесь только REST,
|
// без сканирования списка целиком, плюс REST по «последним N минутам»
|
||||||
// весь список по ApproverId, без архивного Search-довеска.
|
// (Modified) добирает то, что Search мог ещё не проиндексировать. Status
|
||||||
const allRows: Array<Record<string, unknown>> = [];
|
// в обоих случаях берётся СВЕЖИМ через REST по найденным Id (не из индекса
|
||||||
for await (const p of approvalList.items
|
// Search) — именно это делает getInvolvedRowsViaSearch, поэтому решение
|
||||||
.filter(`ApproverId eq ${approver.Id}`)
|
// «моя очередь сейчас» всегда точное, даже когда список кандидатов пришёл
|
||||||
.select('Id', 'TicketId', 'Status')
|
// из Search.
|
||||||
.top(2000)) {
|
|
||||||
allRows.push(...(p as Array<Record<string, unknown>>));
|
|
||||||
}
|
|
||||||
stepRows = allRows.filter(
|
|
||||||
(r) => r.Status === REQUEST_STATUS.onApproval || r.Status === REQUEST_STATUS.onExecution
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[Текущие заявки] «${type.name}»: список согласования «${approvalListTitle}», approver.Id=${approver.Id}, scope=myTurn, строк по ApproverId: ${allRows.length}, после фильтра по статусу: ${stepRows.length}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 'involved' — просто «участвовал ли вообще», Status не важен для
|
|
||||||
// истинности результата, поэтому здесь можно рискнуть Search-ом для
|
|
||||||
// архивной (не только что созданной/изменённой) части: свежее по
|
|
||||||
// Modified — REST, остальное — Search по managed property ApproverOWSUSER
|
|
||||||
// (кастомное поле, не гарантировано, что проиндексировано — см. лог).
|
|
||||||
const cutoffIso = new Date(Date.now() - RECENT_MS).toISOString();
|
const cutoffIso = new Date(Date.now() - RECENT_MS).toISOString();
|
||||||
const recentRows = (await approvalList.items
|
const recentRows = (await approvalList.items
|
||||||
.filter(`ApproverId eq ${approver.Id} and Modified ge '${cutoffIso}'`)
|
.filter(`ApproverId eq ${approver.Id} and Modified ge '${cutoffIso}'`)
|
||||||
.select('Id', 'TicketId', 'Status')
|
.select('Id', 'TicketId', 'Status')
|
||||||
.top(2000)()) as Array<Record<string, unknown>>;
|
.top(2000)()) as Array<Record<string, unknown>>;
|
||||||
|
|
||||||
const archivalRows = await getArchivalInvolvedRows(context, type, approvalList, claimsLogin);
|
const searchRows = await getInvolvedRowsViaSearch(context, type, approvalList, claimsLogin);
|
||||||
const seenIds = new Set(recentRows.map((r) => r.Id));
|
const seenIds = new Set(recentRows.map((r) => r.Id));
|
||||||
stepRows = recentRows.concat(archivalRows.filter((r) => !seenIds.has(r.Id)));
|
const allRows = recentRows.concat(searchRows.filter((r) => !seenIds.has(r.Id)));
|
||||||
|
|
||||||
|
stepRows = allRows.filter(
|
||||||
|
(r) => r.Status === REQUEST_STATUS.onApproval || r.Status === REQUEST_STATUS.onExecution
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`[Текущие заявки] «${type.name}»: список согласования «${approvalListTitle}», approver.Id=${approver.Id}, scope=myTurn — свежих (REST, < 10 мин): ${recentRows.length}, через Search: ${searchRows.length}, после фильтра по статусу: ${stepRows.length}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 'involved' — используется ТОЛЬКО для текстового поиска (см.
|
||||||
|
// getPendingApprovalIndex/searchMyRequests) — здесь важна не мгновенная
|
||||||
|
// свежесть, а полнота по всему архиву, поэтому ищем чисто через Search
|
||||||
|
// по managed property ApproverOWSUSER, без REST-подстраховки на
|
||||||
|
// «последние N минут» — гибрид REST+Search приводил к неустойчивому
|
||||||
|
// результату (заявка то находилась, то нет, в зависимости от того,
|
||||||
|
// успел ли Search её проиндексировать к моменту REST-окна).
|
||||||
|
stepRows = await getInvolvedRowsViaSearch(context, type, approvalList, claimsLogin);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[Текущие заявки] «${type.name}»: список согласования «${approvalListTitle}», approver.Id=${approver.Id}, scope=involved — свежих (REST, < 10 мин): ${recentRows.length}, архивных (Search): ${archivalRows.length}, итого: ${stepRows.length}`
|
`[Текущие заявки] «${type.name}»: список согласования «${approvalListTitle}», approver.Id=${approver.Id}, scope=involved (Search) — найдено: ${stepRows.length}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,15 +254,15 @@ async function indexApprovalsForType(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Архивная (не только что созданная/изменённая) часть 'involved' — через
|
// 'involved' целиком — через Search по managed property ApproverOWSUSER
|
||||||
// Search по managed property ApproverOWSUSER (кастомное Person-поле, НЕТ
|
// (кастомное Person-поле, НЕТ гарантии, что проиндексировано без ручной
|
||||||
// гарантии, что проиндексировано без ручной настройки Search Schema — если
|
// настройки Search Schema — если лог покажет «найдено: 0» при заведомо
|
||||||
// лог покажет «найдено: 0» при заведомо непустом архиве, значит не сработало
|
// непустом архиве, значит не сработало).
|
||||||
// и стоит вернуться на чистый REST для этой ветки).
|
|
||||||
//
|
//
|
||||||
// Search используется ТОЛЬКО чтобы найти Id строк — TicketId и Status берём
|
// Search используется ТОЛЬКО чтобы найти Id строк — TicketId и Status берём
|
||||||
// свежими через REST по этим Id, а не из индекса (индекс мог устареть).
|
// свежими через REST по этим Id (точечное чтение по известным Id — не поиск,
|
||||||
async function getArchivalInvolvedRows(
|
// а обычное дочитывание данных, как и везде в проекте для найденных Search'ом строк).
|
||||||
|
async function getInvolvedRowsViaSearch(
|
||||||
context: WebPartContext,
|
context: WebPartContext,
|
||||||
type: IRequestType,
|
type: IRequestType,
|
||||||
approvalList: IList,
|
approvalList: IList,
|
||||||
@@ -373,23 +376,6 @@ const PAGE_SIZE = 20;
|
|||||||
// (см. filterPendingByQuery) — иначе строка запроса станет неподъёмной.
|
// (см. filterPendingByQuery) — иначе строка запроса станет неподъёмной.
|
||||||
const KQL_ID_LIMIT = 300;
|
const KQL_ID_LIMIT = 300;
|
||||||
|
|
||||||
function isRecent(createdIso: string): boolean {
|
|
||||||
if (!createdIso) return false;
|
|
||||||
const t = new Date(createdIso).getTime();
|
|
||||||
return !isNaN(t) && Date.now() - t <= RECENT_MS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Замена свободному терму Search для случаев, когда матчим на клиенте (без
|
|
||||||
// индекса): все слова запроса должны встретиться в названии (без учёта
|
|
||||||
// регистра). Не идентично семантике Search (там — префикс, тут — подстрока),
|
|
||||||
// но для «довеска» из нескольких свежих строк разница не принципиальна.
|
|
||||||
function matchesQueryTitle(title: string, query: string): boolean {
|
|
||||||
const q = query.trim().toLowerCase();
|
|
||||||
if (!q) return true;
|
|
||||||
const t = title.toLowerCase();
|
|
||||||
return q.split(/\s+/).filter(Boolean).every((word) => t.indexOf(word) !== -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function siteOrigin(siteUrl: string): string {
|
function siteOrigin(siteUrl: string): string {
|
||||||
const match = /^https?:\/\/[^/]+/.exec(siteUrl);
|
const match = /^https?:\/\/[^/]+/.exec(siteUrl);
|
||||||
return match ? match[0] : siteUrl;
|
return match ? match[0] : siteUrl;
|
||||||
@@ -466,15 +452,9 @@ async function loadByIds(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Свежие (созданные за последние RECENT_MS) заявки автора — обычным REST-ом,
|
// Свежие (созданные за последние RECENT_MS) заявки автора — обычным REST-ом,
|
||||||
// в обход Search (индекс мог их ещё не подхватить). Текст запроса матчим на
|
// в обход Search (индекс мог их ещё не подхватить). Без текстового запроса —
|
||||||
// клиенте (matchesQueryTitle) — substringof с кириллицей в $filter уже
|
// матчить название не нужно, отдаём как есть.
|
||||||
// подводил на Status (см. indexApprovalsForType), поэтому в $filter только
|
async function getFreshOwnRowsForType(context: WebPartContext, type: IRequestType): Promise<IMyRequest[]> {
|
||||||
// нечувствительные к этому условия (AuthorId/Created).
|
|
||||||
async function getFreshOwnRowsForType(
|
|
||||||
context: WebPartContext,
|
|
||||||
type: IRequestType,
|
|
||||||
query: string
|
|
||||||
): Promise<IMyRequest[]> {
|
|
||||||
try {
|
try {
|
||||||
const sp = spfi(type.siteUrl as string).using(SPFx(context));
|
const sp = spfi(type.siteUrl as string).using(SPFx(context));
|
||||||
const me = await sp.web.currentUser.select('Id')();
|
const me = await sp.web.currentUser.select('Id')();
|
||||||
@@ -487,24 +467,55 @@ async function getFreshOwnRowsForType(
|
|||||||
.expand('Author')
|
.expand('Author')
|
||||||
.top(50)()) as Array<Record<string, unknown>>;
|
.top(50)()) as Array<Record<string, unknown>>;
|
||||||
|
|
||||||
return rows
|
return rows.map((r) => toMyRequest(r, r.Author as { EMail?: string; Title?: string } | undefined, type, 'own'));
|
||||||
.map((r) => toMyRequest(r, r.Author as { EMail?: string; Title?: string } | undefined, type, 'own'))
|
|
||||||
.filter((r) => matchesQueryTitle(r.title, query));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[Текущие заявки] «${type.name}»: не удалось дочитать свежие заявки:`, (e as Error).message);
|
console.error(`[Текущие заявки] «${type.name}»: не удалось дочитать свежие заявки:`, (e as Error).message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// «Мои заявки»: Search по спискам данных всех типов сразу — автор = текущий
|
// «Мои заявки» БЕЗ поиска — гибрид для масштаба: Search с серверной пагинацией
|
||||||
// пользователь, текст запроса по всем полям. Пагинация серверная
|
// (RowLimit/StartRow) находит страницу без сканирования всего архива, REST по
|
||||||
// (RowLimit/StartRow), поэтому таблица не тянет весь архив заявок.
|
// «последним N минутам» добирает то, что Search мог не успеть проиндексировать
|
||||||
//
|
// (см. RECENT_MS). Довесок применяется только на первой странице — свежее по
|
||||||
// На первой странице довешиваем то, что Search мог ещё не проиндексировать
|
// дате там и оказалось бы, будь оно проиндексировано.
|
||||||
// (см. getFreshOwnRowsForType) — дедуп по (тип, Id), сортировка по дате и
|
|
||||||
// обрезка до PAGE_SIZE. На страницах 2+ этого не делаем: свежее по дате и так
|
|
||||||
// оказалось бы на первой странице, будь оно проиндексировано.
|
|
||||||
export async function getOwnRequests(
|
export async function getOwnRequests(
|
||||||
|
context: WebPartContext,
|
||||||
|
types: IRequestType[],
|
||||||
|
page: number
|
||||||
|
): Promise<IOwnRequestsPage> {
|
||||||
|
if (types.length === 0) return { rows: [], totalRows: 0 };
|
||||||
|
|
||||||
|
const searchPage = await searchOwnRequests(context, types, '', page);
|
||||||
|
|
||||||
|
if (page !== 1) return searchPage;
|
||||||
|
|
||||||
|
const freshRows = (await Promise.all(types.map((t) => getFreshOwnRowsForType(context, t)))).reduce(
|
||||||
|
(all, r) => all.concat(r),
|
||||||
|
[] as IMyRequest[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchKeys = new Set(searchPage.rows.map((r) => `${r.requestTypeId}-${r.id}`));
|
||||||
|
const onlyFresh = freshRows.filter((r) => !searchKeys.has(`${r.requestTypeId}-${r.id}`));
|
||||||
|
|
||||||
|
if (onlyFresh.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`[Текущие заявки] «Мои заявки»: довесили ${onlyFresh.length} свежих заявок (< 10 мин), которых Search ещё не нашёл`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged = onlyFresh
|
||||||
|
.concat(searchPage.rows)
|
||||||
|
.sort((a, b) => b.created.localeCompare(a.created))
|
||||||
|
.slice(0, PAGE_SIZE);
|
||||||
|
|
||||||
|
return { rows: merged, totalRows: searchPage.totalRows + onlyFresh.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
// «Мои заявки» С поиском — только Search, без REST-подстраховки на свежесть:
|
||||||
|
// только что созданная заявка может найтись не сразу (задержка индексации), но
|
||||||
|
// зато результат предсказуем — не зависит от 10-минутного REST-окна.
|
||||||
|
export async function searchOwnRequests(
|
||||||
context: WebPartContext,
|
context: WebPartContext,
|
||||||
types: IRequestType[],
|
types: IRequestType[],
|
||||||
query: string,
|
query: string,
|
||||||
@@ -539,45 +550,16 @@ export async function getOwnRequests(
|
|||||||
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 loadByIds(context, entries, 'own') : new Map<string, IMyRequest>();
|
||||||
// Порядок задаёт релевантность поиска, а не порядок ответов REST.
|
// Порядок задаёт релевантность поиска, а не порядок ответов REST.
|
||||||
const searchRows = entries
|
const rows = entries.map((e) => byKey.get(`${e.type.id}-${e.id}`)).filter((r): r is IMyRequest => !!r);
|
||||||
.map((e) => byKey.get(`${e.type.id}-${e.id}`))
|
|
||||||
.filter((r): r is IMyRequest => !!r);
|
|
||||||
|
|
||||||
if (page !== 1) {
|
return { rows, totalRows: results.TotalRows || 0 };
|
||||||
return { rows: searchRows, totalRows: results.TotalRows || 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`));
|
|
||||||
|
|
||||||
if (onlyFresh.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`[Текущие заявки] «Мои заявки»: довесили ${onlyFresh.length} свежих заявок (< 10 мин), которых Search ещё не нашёл`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const merged = onlyFresh
|
|
||||||
.concat(searchRows)
|
|
||||||
.sort((a, b) => b.created.localeCompare(a.created))
|
|
||||||
.slice(0, PAGE_SIZE);
|
|
||||||
|
|
||||||
return { rows: merged, totalRows: (results.TotalRows || 0) + onlyFresh.length };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// «На согласовании»: список заявок уже отобран по строкам согласования
|
// «На согласовании»: список заявок уже отобран по строкам согласования
|
||||||
// (getPendingApproval/getPendingApprovalIndex — там же определяется, чья
|
// (getPendingApprovalIndex — там же определяется, чья сейчас очередь,
|
||||||
// сейчас очередь, обычным REST). Здесь среди НИХ фильтруем по тексту.
|
// обычным REST). Здесь среди НИХ фильтруем по тексту — только через Search
|
||||||
//
|
// (KQL ограничивается ListItemID этих заявок, текст идёт свободным термом по
|
||||||
// Свежие (< 10 мин, isRecent) заявки Search мог не проиндексировать —
|
// всем полям), без клиентского REST-матчинга по свежести.
|
||||||
// их матчим по названию на клиенте (matchesQueryTitle), без похода в Search.
|
|
||||||
// Остальные — через Search: KQL ограничивается ListItemID этих заявок, текст
|
|
||||||
// идёт свободным термом по всем полям. Порядок сохраняется (rows уже
|
|
||||||
// отсортированы по дате в buildPendingIndex, свежие — в начале).
|
|
||||||
export async function filterPendingByQuery(
|
export async function filterPendingByQuery(
|
||||||
context: WebPartContext,
|
context: WebPartContext,
|
||||||
types: IRequestType[],
|
types: IRequestType[],
|
||||||
@@ -587,21 +569,15 @@ export async function filterPendingByQuery(
|
|||||||
const text = freeTextClause(query);
|
const text = freeTextClause(query);
|
||||||
if (!text || rows.length === 0) return rows;
|
if (!text || rows.length === 0) return rows;
|
||||||
|
|
||||||
const fresh = rows.filter((r) => isRecent(r.created));
|
if (rows.length > KQL_ID_LIMIT) {
|
||||||
const older = rows.filter((r) => !isRecent(r.created));
|
|
||||||
const freshMatches = fresh.filter((r) => matchesQueryTitle(r.title, query));
|
|
||||||
|
|
||||||
if (older.length === 0) return freshMatches;
|
|
||||||
|
|
||||||
if (older.length > KQL_ID_LIMIT) {
|
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Текущие заявки] Поиск среди ждущих решения ограничен ${KQL_ID_LIMIT} заявками из ${older.length}`
|
`[Текущие заявки] Поиск среди ждущих решения ограничен ${KQL_ID_LIMIT} заявками из ${rows.length}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const candidates = older.slice(0, KQL_ID_LIMIT);
|
const candidates = rows.slice(0, KQL_ID_LIMIT);
|
||||||
|
|
||||||
const scopeTypes = types.filter((t) => candidates.some((r) => r.requestTypeId === t.id));
|
const scopeTypes = types.filter((t) => candidates.some((r) => r.requestTypeId === t.id));
|
||||||
if (scopeTypes.length === 0) return freshMatches;
|
if (scopeTypes.length === 0) return [];
|
||||||
|
|
||||||
const sp = spfi(scopeTypes[0].siteUrl as string).using(SPFx(context));
|
const sp = spfi(scopeTypes[0].siteUrl as string).using(SPFx(context));
|
||||||
const kql = [
|
const kql = [
|
||||||
@@ -622,9 +598,7 @@ export async function filterPendingByQuery(
|
|||||||
|
|
||||||
// ListItemID уникален только внутри своего списка — сверяем пару тип+Id.
|
// ListItemID уникален только внутри своего списка — сверяем пару тип+Id.
|
||||||
const found = new Set(hitsToEntries(results, scopeTypes).map((e) => `${e.type.id}-${e.id}`));
|
const found = new Set(hitsToEntries(results, scopeTypes).map((e) => `${e.type.id}-${e.id}`));
|
||||||
const olderMatches = candidates.filter((r) => found.has(`${r.requestTypeId}-${r.id}`));
|
return candidates.filter((r) => found.has(`${r.requestTypeId}-${r.id}`));
|
||||||
|
|
||||||
return freshMatches.concat(olderMatches);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -644,8 +618,8 @@ export interface ISearchPage {
|
|||||||
totalRows: number;
|
totalRows: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Как getOwnRequests, но без $skip/$top — сразу весь найденный срез (до
|
// Как searchOwnRequests, но без $skip/$top — сразу весь найденный срез (до
|
||||||
// KQL_ID_LIMIT), с тем же довеском свежих (< 10 мин) заявок через REST.
|
// KQL_ID_LIMIT). Только Search, без REST-подстраховки на свежесть.
|
||||||
async function searchOwnAll(
|
async function searchOwnAll(
|
||||||
context: WebPartContext,
|
context: WebPartContext,
|
||||||
types: IRequestType[],
|
types: IRequestType[],
|
||||||
@@ -675,16 +649,7 @@ async function searchOwnAll(
|
|||||||
|
|
||||||
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 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);
|
return 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(
|
export async function searchMyRequests(
|
||||||
@@ -713,7 +678,7 @@ export async function searchMyRequests(
|
|||||||
const pageRows = merged.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
const pageRows = merged.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||||
|
|
||||||
// Автор нужен для 'approval'-строк текущей страницы — 'own' уже пришли с
|
// Автор нужен для 'approval'-строк текущей страницы — 'own' уже пришли с
|
||||||
// автором (loadByIds/getFreshOwnRowsForType сами дочитывают Author).
|
// автором (loadByIds сам дочитывает Author).
|
||||||
const needAuthor = pageRows.filter((r) => r.kind === 'approval');
|
const needAuthor = pageRows.filter((r) => r.kind === 'approval');
|
||||||
if (needAuthor.length > 0) {
|
if (needAuthor.length > 0) {
|
||||||
await hydratePendingAuthors(context, involvedIdx.types, needAuthor);
|
await hydratePendingAuthors(context, involvedIdx.types, needAuthor);
|
||||||
|
|||||||
@@ -38,13 +38,14 @@ export class ApproverService {
|
|||||||
public async getPendingRowForCurrentUser(requestId: number): Promise<IApprovalRow | undefined> {
|
public async getPendingRowForCurrentUser(requestId: number): Promise<IApprovalRow | undefined> {
|
||||||
const rows = await this.approval.getForRequest(requestId);
|
const rows = await this.approval.getForRequest(requestId);
|
||||||
const userId = await this.approval.getCurrentUserId();
|
const userId = await this.approval.getCurrentUserId();
|
||||||
// Активна первая незакрытая строка по очереди: согласующий («На согласовании»)
|
// Активные строки текущего шага: согласующий («На согласовании») или, когда
|
||||||
// или, когда согласование пройдено, исполнитель («На исполнении»).
|
// согласование пройдено, исполнитель («На исполнении»). При параллельном шаге
|
||||||
const current = rows.filter(
|
// (одинаковый StepOrder) активных строк может быть несколько сразу — ищем
|
||||||
|
// среди ВСЕХ активных, а не только среди первой по порядку.
|
||||||
|
const active = rows.filter(
|
||||||
(r) => r.status === REQUEST_STATUS.onApproval || r.status === REQUEST_STATUS.onExecution
|
(r) => r.status === REQUEST_STATUS.onApproval || r.status === REQUEST_STATUS.onExecution
|
||||||
)[0];
|
);
|
||||||
if (current && current.approverId === userId) return current;
|
return active.filter((r) => r.approverId === userId)[0];
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Делегирование: согласующий добавляет делеганта ПЕРЕД собой (Order — между
|
// Делегирование: согласующий добавляет делеганта ПЕРЕД собой (Order — между
|
||||||
|
|||||||
Reference in New Issue
Block a user