различные кэши
This commit is contained in:
@@ -3,7 +3,7 @@ import styles from './RequestPortal.module.scss';
|
||||
import type { IRequestPortalProps } from './IRequestPortalProps';
|
||||
import { IconSprite } from '../shared/ui/Icon/IconSprite';
|
||||
import { navigate, useRoute, NavSection } from '../shared/routing';
|
||||
import { onRequestsChanged } from '../shared/requestEvents';
|
||||
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
|
||||
import { SideNav } from './SideNav/SideNav';
|
||||
import { Drawer } from './Drawer/Drawer';
|
||||
import { CategoryTabs } from './CategoryTabs/CategoryTabs';
|
||||
@@ -75,7 +75,14 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
}
|
||||
}, [route.mode, route.formKey]);
|
||||
|
||||
const goto = (nav: NavSection): void => navigate({ mode: 'submit', nav });
|
||||
const goto = (nav: NavSection): void => {
|
||||
// Клик по УЖЕ активному пункту меню не меняет маршрут, значит эффект
|
||||
// загрузки в таблице не перезапустится сам. Шлём событие обновления —
|
||||
// таблица на него подписана и перечитает данные фоном, не убирая с экрана
|
||||
// текущие строки (см. RequestsTableView/SearchView).
|
||||
if (nav === route.nav && route.mode === 'submit') emitRequestsChanged();
|
||||
navigate({ mode: 'submit', nav });
|
||||
};
|
||||
const closeDrawer = (): void => navigate({ mode: 'submit', nav: route.nav });
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IMyRequest } from './models/IMyRequest';
|
||||
import { REQUEST_STATUS, IRequestType } from '../../shared/approval';
|
||||
import { onRequestsChanged } from '../../shared/requestEvents';
|
||||
import { usePersistedQuery, useDebouncedQuery } from '../../shared/usePersistedQuery';
|
||||
import { IPage, pageKey, pageSignature, readPage, writePage } from './pageCache';
|
||||
import { Pager } from './Pager';
|
||||
import styles from './RequestsTableView.module.scss';
|
||||
|
||||
@@ -177,49 +178,58 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
|
||||
React.useEffect(() => {
|
||||
if (types === undefined) return;
|
||||
let cancelled = false;
|
||||
const cacheKey = pageKey('table', [kind, debouncedQuery.trim(), page]);
|
||||
|
||||
// Сразу показываем то, что уже загружали для ЭТОЙ вкладки (и этого запроса
|
||||
// и страницы), а обновление идёт фоном. Компонент один на обе вкладки —
|
||||
// без кеша при переключении на экране оставались бы строки предыдущей
|
||||
// вкладки, пока не придёт ответ. Если для этой вкладки данных ещё нет —
|
||||
// rows становится undefined, и показывается «Загрузка…».
|
||||
const cached = readPage(cacheKey);
|
||||
setRows(cached ? cached.rows : undefined);
|
||||
setTotalRows(cached ? cached.totalRows : 0);
|
||||
|
||||
const fetchPage = async (): Promise<IPage> => {
|
||||
if (kind === 'own') {
|
||||
return debouncedQuery.trim().length > 0
|
||||
? searchOwnRequests(context, types, debouncedQuery, page)
|
||||
: getOwnRequests(context, types, page);
|
||||
}
|
||||
|
||||
if (debouncedQuery.trim().length === 0) {
|
||||
// Без поиска — страница уже нарезана и дочитана сервером.
|
||||
const r = await getPendingApproval(context, 'myTurn', page);
|
||||
return { rows: r.rows, totalRows: r.totalRows };
|
||||
}
|
||||
|
||||
// С поиском — сначала полный список «где я есть в маршруте» (без
|
||||
// автора), фильтруем текстом, и только потом режем на страницы и
|
||||
// дочитываем автора — только для видимой страницы.
|
||||
const idx = await getPendingApprovalIndex(context, 'involved');
|
||||
const filtered = await filterPendingByQuery(context, idx.types, idx.rows, debouncedQuery);
|
||||
const pageRows = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||
await hydratePendingAuthors(context, idx.types, pageRows);
|
||||
return { rows: pageRows, totalRows: filtered.length };
|
||||
};
|
||||
|
||||
const load = async (): Promise<void> => {
|
||||
try {
|
||||
if (kind === 'own') {
|
||||
const hasOwnQuery = debouncedQuery.trim().length > 0;
|
||||
const r = hasOwnQuery
|
||||
? await searchOwnRequests(context, types, debouncedQuery, page)
|
||||
: await getOwnRequests(context, types, page);
|
||||
if (cancelled) return;
|
||||
setRows(r.rows);
|
||||
setTotalRows(r.totalRows);
|
||||
setError('');
|
||||
return;
|
||||
const fresh = await fetchPage();
|
||||
if (cancelled) return;
|
||||
|
||||
// Перерисовываем, только если данные реально изменились — иначе при
|
||||
// каждом возврате на вкладку таблица моргала бы, перерисовываясь тем же
|
||||
// самым содержимым.
|
||||
const prev = readPage(cacheKey);
|
||||
if (!prev || pageSignature(prev) !== pageSignature(fresh)) {
|
||||
writePage(cacheKey, fresh);
|
||||
setRows(fresh.rows);
|
||||
setTotalRows(fresh.totalRows);
|
||||
}
|
||||
|
||||
const hasQuery = debouncedQuery.trim().length > 0;
|
||||
|
||||
if (!hasQuery) {
|
||||
// Без поиска — страница уже нарезана и дочитана сервером.
|
||||
const r = await getPendingApproval(context, 'myTurn', page);
|
||||
if (cancelled) return;
|
||||
console.table(r.debug);
|
||||
setRows(r.rows);
|
||||
setTotalRows(r.totalRows);
|
||||
setError('');
|
||||
if (onMyTurnCountChange) onMyTurnCountChange(r.totalRows);
|
||||
return;
|
||||
}
|
||||
|
||||
// С поиском — сначала полный список «где я есть в маршруте» (без
|
||||
// автора), фильтруем текстом, и только потом режем на страницы и
|
||||
// дочитываем автора — только для видимой страницы.
|
||||
const idx = await getPendingApprovalIndex(context, 'involved');
|
||||
if (cancelled) return;
|
||||
console.table(idx.debug);
|
||||
const filtered = await filterPendingByQuery(context, idx.types, idx.rows, debouncedQuery);
|
||||
if (cancelled) return;
|
||||
const pageRows = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||
await hydratePendingAuthors(context, idx.types, pageRows);
|
||||
if (cancelled) return;
|
||||
setRows(pageRows);
|
||||
setTotalRows(filtered.length);
|
||||
setError('');
|
||||
if (onMyTurnCountChange && kind === 'approvals' && debouncedQuery.trim().length === 0) {
|
||||
onMyTurnCountChange(fresh.totalRows);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled) setError((e as Error).message);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IMyRequest } from './models/IMyRequest';
|
||||
import { REQUEST_STATUS, IRequestType } from '../../shared/approval';
|
||||
import { onRequestsChanged } from '../../shared/requestEvents';
|
||||
import { usePersistedQuery, useDebouncedQuery } from '../../shared/usePersistedQuery';
|
||||
import { pageKey, pageSignature, readPage, writePage } from './pageCache';
|
||||
import { Pager } from './Pager';
|
||||
import styles from './SearchView.module.scss';
|
||||
|
||||
@@ -140,13 +141,24 @@ export const SearchView: React.FC<ISearchViewProps> = ({ context, onOpen }) => {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const cacheKey = pageKey('search', [debouncedQuery.trim(), page]);
|
||||
|
||||
// Уже искали это раньше — показываем прежний результат сразу, обновление
|
||||
// идёт фоном (см. RequestsTableView, там та же схема и те же причины).
|
||||
const cached = readPage(cacheKey);
|
||||
setRows(cached ? cached.rows : undefined);
|
||||
setTotalRows(cached ? cached.totalRows : 0);
|
||||
|
||||
const load = async (): Promise<void> => {
|
||||
try {
|
||||
const r = await searchMyRequests(context, types, debouncedQuery, page);
|
||||
const fresh = await searchMyRequests(context, types, debouncedQuery, page);
|
||||
if (cancelled) return;
|
||||
setRows(r.rows);
|
||||
setTotalRows(r.totalRows);
|
||||
const prev = readPage(cacheKey);
|
||||
if (!prev || pageSignature(prev) !== pageSignature(fresh)) {
|
||||
writePage(cacheKey, fresh);
|
||||
setRows(fresh.rows);
|
||||
setTotalRows(fresh.totalRows);
|
||||
}
|
||||
setError('');
|
||||
} catch (e) {
|
||||
if (!cancelled) setError((e as Error).message);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { IMyRequest } from './models/IMyRequest';
|
||||
|
||||
// Загруженная страница таблицы — то, что кладётся в кеш вкладки
|
||||
// (см. RequestsTableView/SearchView).
|
||||
export interface IPage {
|
||||
rows: IMyRequest[];
|
||||
totalRows: number;
|
||||
}
|
||||
|
||||
// Хранилище на уровне МОДУЛЯ, а не внутри компонента (useRef), потому что
|
||||
// компоненты таблиц размонтируются при переключении разделов: RequestPortal
|
||||
// рендерит либо SubmitFeature, либо SearchView, либо RequestsTableView — уход
|
||||
// на «Поиск» или «Новая заявка» уничтожает экземпляр таблицы вместе со всем его
|
||||
// состоянием. С useRef кеш переживал только переключение «Мои заявки» ↔ «На
|
||||
// согласовании» (там экземпляр один, меняется лишь проп kind), а после захода
|
||||
// на «Поиск» терялся. Здесь он живёт до перезагрузки страницы.
|
||||
//
|
||||
// Ключ формирует вызывающая сторона и обязательно начинает с префикса раздела
|
||||
// (см. pageKey), иначе ключи двух таблиц пересеклись бы.
|
||||
const store = new Map<string, IPage>();
|
||||
|
||||
// Ключ кеша: раздел + всё, от чего зависит содержимое страницы.
|
||||
export function pageKey(view: string, parts: Array<string | number>): string {
|
||||
return [view, ...parts].join('|');
|
||||
}
|
||||
|
||||
export function readPage(key: string): IPage | undefined {
|
||||
return store.get(key);
|
||||
}
|
||||
|
||||
export function writePage(key: string, page: IPage): void {
|
||||
store.set(key, page);
|
||||
}
|
||||
|
||||
// Слепок страницы для сравнения «изменилось ли что-нибудь». Сравниваем поля,
|
||||
// которые видны в таблице: если они совпали, перерисовывать нечего — иначе при
|
||||
// каждом возврате на вкладку и при каждом фоновом обновлении таблица моргала бы,
|
||||
// перерисовываясь тем же самым содержимым.
|
||||
export function pageSignature(page: IPage): string {
|
||||
return `${page.totalRows}|${page.rows
|
||||
.map((r) => [r.requestTypeId, r.id, r.title, r.status, r.created, r.authorName].join('~'))
|
||||
.join(';')}`;
|
||||
}
|
||||
Reference in New Issue
Block a user