From bd3689cf14b74a3997ffee22a200a0460b9e633b Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Thu, 6 Aug 2026 14:55:46 +0500 Subject: [PATCH] =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=BB=D0=B8=D1=87=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BA=D1=8D=D1=88=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/RequestPortal.tsx | 11 ++- .../features/myRequests/RequestsTableView.tsx | 86 +++++++++++-------- .../features/myRequests/SearchView.tsx | 18 +++- .../features/myRequests/pageCache.ts | 43 ++++++++++ 4 files changed, 115 insertions(+), 43 deletions(-) create mode 100644 src/webparts/requestPortal/features/myRequests/pageCache.ts diff --git a/src/webparts/requestPortal/components/RequestPortal.tsx b/src/webparts/requestPortal/components/RequestPortal.tsx index d675a3b..5a7cb66 100644 --- a/src/webparts/requestPortal/components/RequestPortal.tsx +++ b/src/webparts/requestPortal/components/RequestPortal.tsx @@ -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 = ({ 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 ( diff --git a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx index ee191d9..db30b14 100644 --- a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx +++ b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx @@ -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 = ({ 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 => { + 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 => { 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); } diff --git a/src/webparts/requestPortal/features/myRequests/SearchView.tsx b/src/webparts/requestPortal/features/myRequests/SearchView.tsx index f20f2ef..063e403 100644 --- a/src/webparts/requestPortal/features/myRequests/SearchView.tsx +++ b/src/webparts/requestPortal/features/myRequests/SearchView.tsx @@ -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 = ({ 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 => { 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); diff --git a/src/webparts/requestPortal/features/myRequests/pageCache.ts b/src/webparts/requestPortal/features/myRequests/pageCache.ts new file mode 100644 index 0000000..8159322 --- /dev/null +++ b/src/webparts/requestPortal/features/myRequests/pageCache.ts @@ -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(); + +// Ключ кеша: раздел + всё, от чего зависит содержимое страницы. +export function pageKey(view: string, parts: Array): 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(';')}`; +}