From 4fee057f1596e3be39a37c861aef99f97df6eded Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Tue, 18 Aug 2026 12:10:10 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../requestPortal/RequestPortalWebPart.ts | 5 +++++ .../components/RequestPortal.module.scss | 15 +++++++++++++-- .../components/RequestPortal.tsx | 2 +- .../features/myRequests/RequestsTableView.tsx | 9 ++++++++- .../features/myRequests/SearchView.tsx | 3 ++- .../requestPortal/shared/requestEvents.ts | 19 ++++++++++++++----- 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/webparts/requestPortal/RequestPortalWebPart.ts b/src/webparts/requestPortal/RequestPortalWebPart.ts index bd2fc70..d359f9d 100644 --- a/src/webparts/requestPortal/RequestPortalWebPart.ts +++ b/src/webparts/requestPortal/RequestPortalWebPart.ts @@ -35,6 +35,11 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart(RequestPortal, { + // key на подменённом e-mail: смена подмены в property pane должна + // пересобрать раздел заново — иначе счётчики в боковом меню и открытые + // таблицы остались бы от прежнего пользователя (их эффекты завязаны на + // монтирование/context, а не на identity подмены). + key: this.properties.debugUserEmail || '__self__', isDarkTheme: this._isDarkTheme, environmentMessage: this._environmentMessage, hasTeamsContext: !!this.context.sdks.microsoftTeams, diff --git a/src/webparts/requestPortal/components/RequestPortal.module.scss b/src/webparts/requestPortal/components/RequestPortal.module.scss index b34cbb5..9aa1ad4 100644 --- a/src/webparts/requestPortal/components/RequestPortal.module.scss +++ b/src/webparts/requestPortal/components/RequestPortal.module.scss @@ -50,15 +50,26 @@ // Раскладка: боковой рейл (side) на всю высоту + основная область (main). .layout { display: flex; - align-items: stretch; // рейл тянется на высоту контента + align-items: flex-start; // рейл больше НЕ тянется по контенту — см. .side width: 100%; min-height: 78vh; // «присутствие» рейла даже когда контента мало } -// Рейл-колонка: сам Sidebar тянется по высоте (flex-стретч). +// Рейл-колонка: раньше тянулась по высоте .main (align-items: stretch), из-за +// чего высота рейла плавала вместе с содержимым соседней колонки — например, у +// вкладки «Инициированные мной» появляется доп. фильтр по статусу, .main +// становится чуть выше, и градиентный фон Sidebar (посчитанный от своей же +// высоты) заметно менял оттенок. Прибиваем рейл к высоте окна (sticky + 100vh): +// теперь он не зависит от того, что показано рядом. На мобильном брейкпоинте +// сам Sidebar уходит в position:fixed (см. @tnsu/ui-kit) — там эти правила уже +// не действуют. .side { flex: 0 0 auto; display: flex; + position: sticky; + top: 0; + align-self: flex-start; + height: 100vh; } .main { diff --git a/src/webparts/requestPortal/components/RequestPortal.tsx b/src/webparts/requestPortal/components/RequestPortal.tsx index 92a8006..f98c872 100644 --- a/src/webparts/requestPortal/components/RequestPortal.tsx +++ b/src/webparts/requestPortal/components/RequestPortal.tsx @@ -134,7 +134,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context // загрузки в таблице не перезапустится сам. Шлём событие обновления — // таблица на него подписана и перечитает данные фоном, не убирая с экрана // текущие строки (см. RequestsTableView/SearchView). - if (nav === route.nav && route.mode === 'submit') emitRequestsChanged(); + if (nav === route.nav && route.mode === 'submit') emitRequestsChanged(true); // Всё восстанавливаемое состояние раздела передаём ЯВНО, из запомненного. // parseRoute подставил бы его и сам, но тогда оно осталось бы только в // состоянии и не попало бы в адрес: ссылка на раздел вела бы на первую diff --git a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx index 2a96cf0..9ec124b 100644 --- a/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx +++ b/src/webparts/requestPortal/features/myRequests/RequestsTableView.tsx @@ -416,7 +416,14 @@ export const RequestsTableView: React.FC = ({ }; load().catch(() => undefined); // ошибки уже обработаны внутри load через setError - const unsubscribe = onRequestsChanged(() => { + const unsubscribe = onRequestsChanged((force) => { + // Явный повторный клик по уже открытому пункту меню — пользователь ждёт + // видимого обновления, а не молчаливой подмены данных за спиной. Прежние + // строки прячем сразу; свежие подставит load(). + if (force) { + setRows(undefined); + setLoadedKey(''); + } load().catch(() => undefined); }); // Пока вкладка браузера скрыта, обновляться незачем — при возврате к ней diff --git a/src/webparts/requestPortal/features/myRequests/SearchView.tsx b/src/webparts/requestPortal/features/myRequests/SearchView.tsx index 7038d4a..75bdca8 100644 --- a/src/webparts/requestPortal/features/myRequests/SearchView.tsx +++ b/src/webparts/requestPortal/features/myRequests/SearchView.tsx @@ -200,7 +200,8 @@ export const SearchView: React.FC = ({ }; load().catch(() => undefined); - const unsubscribe = onRequestsChanged(() => { + const unsubscribe = onRequestsChanged((force) => { + if (force) setRows(undefined); load().catch(() => undefined); }); const timer = window.setInterval(() => { diff --git a/src/webparts/requestPortal/shared/requestEvents.ts b/src/webparts/requestPortal/shared/requestEvents.ts index 80ee54a..8a9beea 100644 --- a/src/webparts/requestPortal/shared/requestEvents.ts +++ b/src/webparts/requestPortal/shared/requestEvents.ts @@ -4,12 +4,21 @@ // достаточно для нечастых событий (сохранение формы, решение согласующего). const EVENT_NAME = 'rp-requests-changed'; -export function emitRequestsChanged(): void { - window.dispatchEvent(new Event(EVENT_NAME)); +/** + * force=true — повторный клик по УЖЕ открытому пункту меню (см. goto в + * RequestPortal.tsx): пользователь явно просит обновить список, поэтому таблица + * прячет прежние строки и показывает «Загрузка…», пока не придёт свежий ответ. + * force=false/не задан — фоновые обновления (создание заявки, решение + * согласующего, десятиминутный автообновление): строки остаются на экране, чтобы + * не моргать, если данные не изменились. + */ +export function emitRequestsChanged(force?: boolean): void { + window.dispatchEvent(new CustomEvent<{ force?: boolean }>(EVENT_NAME, { detail: { force } })); } /** Подписка на изменения. Возвращает функцию отписки (вызвать в cleanup эффекта). */ -export function onRequestsChanged(handler: () => void): () => void { - window.addEventListener(EVENT_NAME, handler); - return () => window.removeEventListener(EVENT_NAME, handler); +export function onRequestsChanged(handler: (force?: boolean) => void): () => void { + const listener = (e: Event): void => handler((e as CustomEvent<{ force?: boolean }>).detail?.force); + window.addEventListener(EVENT_NAME, listener); + return () => window.removeEventListener(EVENT_NAME, listener); }