This commit is contained in:
@@ -353,18 +353,26 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
||||
|
||||
const q = debouncedQuery.trim();
|
||||
|
||||
const fetchRows = async (): Promise<IPage> => {
|
||||
// onPartial — Search и REST-довесок по свежести («Мои заявки»/«Доработка»,
|
||||
// см. getOwnRequests/getOwnRequestsByStatus в myRequests.service.ts) теперь
|
||||
// идут параллельно, и заранее не угадать, какой из двух ответит первым —
|
||||
// сеть шумит то в одну, то в другую сторону. onPartial зовётся с тем, что
|
||||
// пришло раньше, чтобы не держать список пустым ради заведомо более
|
||||
// медленного источника. Остальные ветки (approvals/execution/поиск) его
|
||||
// просто не принимают — там нет пары «Search + довесок» с независимо
|
||||
// валидным промежуточным результатом.
|
||||
const fetchRows = async (onPartial?: (p: IPage) => void): Promise<IPage> => {
|
||||
// «Доработка» — тот же список, что «Инициированные», просто со статусом,
|
||||
// зашитым в раздел, а не выбранным в фильтре.
|
||||
if (view === 'rework' || (view === 'own:created' && statusFilter)) {
|
||||
const status = view === 'rework' ? REQUEST_STATUS.returned : statusFilter;
|
||||
return getOwnRequestsByStatus(context, types, status, page, debouncedQuery);
|
||||
return getOwnRequestsByStatus(context, types, status, page, debouncedQuery, onPartial);
|
||||
}
|
||||
|
||||
if (view === 'own:created') {
|
||||
return q.length > 0
|
||||
? searchOwnRequests(context, types, debouncedQuery, page)
|
||||
: getOwnRequests(context, types, page);
|
||||
: getOwnRequests(context, types, page, onPartial);
|
||||
}
|
||||
|
||||
const queue = QUEUE_BY_VIEW[view];
|
||||
@@ -387,9 +395,11 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
||||
// Комментарий к решению дочитываем ОДНИМ местом на все ветки: «Доработка» и
|
||||
// «Отклонённые мной» приходят разными путями (по автору и по строкам
|
||||
// маршрута соответственно), а колонка у них одинаковая по смыслу. И только
|
||||
// для видимой страницы — как и автор в hydratePendingAuthors.
|
||||
const fetchPage = async (): Promise<IPage> => {
|
||||
const result = await fetchRows();
|
||||
// для видимой страницы — как и автор в hydratePendingAuthors. Промежуточный
|
||||
// показ (onPartial) комментарий ещё не несёт — он дочитывается только для
|
||||
// ИТОГОВОГО результата, полный показ его донесёт следом.
|
||||
const fetchPage = async (onPartial?: (p: IPage) => void): Promise<IPage> => {
|
||||
const result = await fetchRows(onPartial);
|
||||
const decisionStatus = decisionStatusFor(view, statusFilter);
|
||||
if (decisionStatus) {
|
||||
await hydrateDecisionComments(context, types, result.rows, decisionStatus);
|
||||
@@ -399,7 +409,23 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
||||
|
||||
const load = async (forceRender = false): Promise<void> => {
|
||||
try {
|
||||
const fresh = await perfScope(`таблица ${view} стр.${page}`, fetchPage);
|
||||
// Промежуточный показ — ТОЛЬКО когда для этой страницы прямо СЕЙЧАС
|
||||
// ничего не закешировано (первый заход, экран пуст). Проверяем на
|
||||
// каждый вызов load(), а не один раз на весь эффект: тот же load()
|
||||
// переиспользуется автообновлением по таймеру и событием
|
||||
// rp-requests-changed все 10 минут жизни вкладки, и ко второму вызову
|
||||
// кеш почти наверняка уже есть — без повторной проверки список бы на
|
||||
// мгновение "усыхал" при КАЖДОМ фоновом обновлении, а не только при
|
||||
// самой первой загрузке.
|
||||
const onPartial = readPage(cacheKey)
|
||||
? undefined
|
||||
: (partial: IPage): void => {
|
||||
if (cancelled) return;
|
||||
setRows(partial.rows);
|
||||
setTotalRows(partial.totalRows);
|
||||
};
|
||||
|
||||
const fresh = await perfScope(`таблица ${view} стр.${page}`, () => fetchPage(onPartial));
|
||||
if (cancelled) return;
|
||||
|
||||
// Перерисовываем, только если данные реально изменились — иначе при
|
||||
|
||||
@@ -1252,37 +1252,85 @@ async function getRecentlyChangedOwnRowsForType(context: WebPartContext, type: I
|
||||
}
|
||||
}
|
||||
|
||||
// Итог слияния Search-страницы с REST-довеском по свежести — общая логика для
|
||||
// getOwnRequests и ownRequestsByStatus. Довесок ДОПОЛНЯЕТ Search, а не заменяет:
|
||||
// то, что Search уже нашёл, в приоритете (там могло появиться более точное
|
||||
// совпадение по совместному запросу — статус/текст), довесок добавляет только
|
||||
// то, чего в найденном ещё нет.
|
||||
function mergeSearchAndFresh(searchPage: IOwnRequestsPage, freshRows: IMyRequest[]): IOwnRequestsPage {
|
||||
const searchKeys = new Set(searchPage.rows.map((r) => rowKey(r)));
|
||||
const onlyFresh = freshRows.filter((r) => !searchKeys.has(rowKey(r)));
|
||||
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-довесок по свежести ПАРАЛЛЕЛЬНО — то один
|
||||
* отвечает быстрее, то другой, заранее не угадать, и ждать заведомо более
|
||||
* медленный, держа экран пустым, незачем.
|
||||
*
|
||||
* Если задан onPartial — зовёт его РАЗ, как только придёт ПЕРВЫЙ из двух
|
||||
* источников (та половина, что уже есть, без дедупа со вторым — его мы ещё не
|
||||
* видели). Возвращает функция в любом случае ИТОГОВЫЙ слитый результат;
|
||||
* onPartial — только для промежуточного показа, на итог не влияет.
|
||||
*
|
||||
* Без onPartial ведёт себя как раньше: ждёт оба источника и сразу отдаёт слитый
|
||||
* результат — второй показ (полный) экономить незачем, если промежуточный
|
||||
* всё равно никто не увидит.
|
||||
*/
|
||||
async function raceThenMerge(
|
||||
searchPromise: Promise<IOwnRequestsPage>,
|
||||
freshPromise: Promise<IMyRequest[]>,
|
||||
onPartial: ((page: IOwnRequestsPage) => void) | undefined
|
||||
): Promise<IOwnRequestsPage> {
|
||||
if (onPartial) {
|
||||
let searchDone = false;
|
||||
let freshDone = false;
|
||||
// Ошибку здесь не обрабатываем — её поймает Promise.all ниже; этот
|
||||
// отдельный .then только подсматривает за тем, что пришло раньше.
|
||||
searchPromise.then((r) => {
|
||||
searchDone = true;
|
||||
if (!freshDone) onPartial(r);
|
||||
}, () => undefined);
|
||||
freshPromise.then((r) => {
|
||||
freshDone = true;
|
||||
if (!searchDone) onPartial({ rows: r, totalRows: r.length });
|
||||
}, () => undefined);
|
||||
}
|
||||
|
||||
const [searchPage, freshRows] = await Promise.all([searchPromise, freshPromise]);
|
||||
return mergeSearchAndFresh(searchPage, freshRows);
|
||||
}
|
||||
|
||||
// «Мои заявки» БЕЗ поиска — гибрид для масштаба: Search с серверной пагинацией
|
||||
// (RowLimit/StartRow) находит страницу без сканирования всего архива, REST по
|
||||
// «последним N минутам» добирает то, что Search мог не успеть проиндексировать
|
||||
// (см. RECENT_MS). Довесок применяется только на первой странице — свежее по
|
||||
// дате там и оказалось бы, будь оно проиндексировано.
|
||||
//
|
||||
// Search и довесок идут ПАРАЛЛЕЛЬНО (см. raceThenMerge) — раньше довесок
|
||||
// стартовал только ПОСЛЕ ответа Search, хотя они независимы друг от друга и
|
||||
// читают разные источники; тем самым просто складывалось время обоих вместо
|
||||
// времени более медленного из двух.
|
||||
export async function getOwnRequests(
|
||||
context: WebPartContext,
|
||||
types: IRequestType[],
|
||||
page: number
|
||||
page: number,
|
||||
onPartial?: (page: IOwnRequestsPage) => void
|
||||
): Promise<IOwnRequestsPage> {
|
||||
if (types.length === 0) return { rows: [], totalRows: 0 };
|
||||
|
||||
const searchPage = await searchOwnRequests(context, types, '', page);
|
||||
const searchPromise = searchOwnRequests(context, types, '', page);
|
||||
if (page !== 1) return searchPromise;
|
||||
|
||||
if (page !== 1) return searchPage;
|
||||
const freshPromise = Promise.all(types.map((t) => getFreshOwnRowsForType(context, t))).then((r) =>
|
||||
r.reduce((all, x) => all.concat(x), [] as IMyRequest[])
|
||||
);
|
||||
|
||||
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) => rowKey(r)));
|
||||
const onlyFresh = freshRows.filter((r) => !searchKeys.has(rowKey(r)));
|
||||
|
||||
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 };
|
||||
return raceThenMerge(searchPromise, freshPromise, onPartial);
|
||||
}
|
||||
|
||||
// «Мои заявки» С поиском — только Search, без REST-подстраховки на свежесть:
|
||||
@@ -1339,9 +1387,10 @@ export async function getOwnRequestsByStatus(
|
||||
types: IRequestType[],
|
||||
status: string,
|
||||
page: number,
|
||||
query: string = ''
|
||||
query: string = '',
|
||||
onPartial?: (page: IOwnRequestsPage) => void
|
||||
): Promise<IOwnRequestsPage> {
|
||||
return ownRequestsByStatus(context, types, status, page, query);
|
||||
return ownRequestsByStatus(context, types, status, page, query, onPartial);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1370,7 +1419,8 @@ async function ownRequestsByStatus(
|
||||
types: IRequestType[],
|
||||
status: string,
|
||||
page: number,
|
||||
query: string
|
||||
query: string,
|
||||
onPartial?: (page: IOwnRequestsPage) => void
|
||||
): Promise<IOwnRequestsPage> {
|
||||
if (types.length === 0) return { rows: [], totalRows: 0 };
|
||||
|
||||
@@ -1385,7 +1435,7 @@ async function ownRequestsByStatus(
|
||||
if (text) clauses.push(text);
|
||||
const kql = clauses.join(' AND ');
|
||||
|
||||
const results = await traced('Search: свои заявки', () =>
|
||||
const searchPromise: Promise<IOwnRequestsPage> = traced('Search: свои заявки', () =>
|
||||
sp.search({
|
||||
Querytext: kql,
|
||||
RowLimit: PAGE_SIZE,
|
||||
@@ -1395,34 +1445,24 @@ async function ownRequestsByStatus(
|
||||
// Свежие сверху — как в searchOwnRequests.
|
||||
SortList: [{ Property: 'Created', Direction: 1 }],
|
||||
})
|
||||
);
|
||||
|
||||
const totalRows = results.TotalRows || 0;
|
||||
const entries = hitsToEntries(results, types);
|
||||
const searchRows = entries.map(toMyRequestFromSearch);
|
||||
|
||||
if (page !== 1 || query.trim()) return { rows: searchRows, totalRows };
|
||||
).then((results) => ({
|
||||
rows: hitsToEntries(results, types).map(toMyRequestFromSearch),
|
||||
totalRows: results.TotalRows || 0,
|
||||
}));
|
||||
|
||||
// Довесок по недавним изменениям — только на первой странице и только без
|
||||
// текстового поиска (тот же принцип, что в getOwnRequests): заявку могли
|
||||
// вернуть на доработку только что, индекс мог не успеть её подхватить.
|
||||
const freshRows = (
|
||||
await Promise.all(
|
||||
types.map((t) => getRecentlyChangedOwnRowsForType(context, t))
|
||||
)
|
||||
)
|
||||
.reduce((acc, r) => acc.concat(r), [] as IMyRequest[])
|
||||
.filter((r) => r.status === status);
|
||||
// Запускаем СРАЗУ, не дожидаясь Search: условие включения известно из
|
||||
// параметров вызова, ждать ради него ответ Search незачем — раньше довесок
|
||||
// стартовал только ПОСЛЕ Search, хотя независим от его результата.
|
||||
if (page !== 1 || query.trim()) return searchPromise;
|
||||
|
||||
const searchKeys = new Set(entries.map((e) => requestKey(e.target.listId, e.id)));
|
||||
const onlyFresh = freshRows.filter((r) => !searchKeys.has(rowKey(r)));
|
||||
const freshPromise = Promise.all(types.map((t) => getRecentlyChangedOwnRowsForType(context, t))).then((r) =>
|
||||
r.reduce((acc, x) => acc.concat(x), [] as IMyRequest[]).filter((x) => x.status === status)
|
||||
);
|
||||
|
||||
const merged = onlyFresh
|
||||
.concat(searchRows)
|
||||
.sort((a, b) => b.created.localeCompare(a.created))
|
||||
.slice(0, PAGE_SIZE);
|
||||
|
||||
return { rows: merged, totalRows: totalRows + onlyFresh.length };
|
||||
return raceThenMerge(searchPromise, freshPromise, onPartial);
|
||||
}
|
||||
|
||||
// Сколько строк маршрута максимум забираем за раз при дочитывании комментариев:
|
||||
|
||||
Reference in New Issue
Block a user