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