Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/5
This commit was merged in pull request #5.
This commit is contained in:
@@ -2,7 +2,20 @@ import * as React from 'react';
|
||||
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 {
|
||||
navigate,
|
||||
persistOwnTab,
|
||||
persistOwnStatus,
|
||||
persistPage,
|
||||
persistQuery,
|
||||
storedOwnStatus,
|
||||
storedOwnTab,
|
||||
storedPageFor,
|
||||
storedQueryFor,
|
||||
useRoute,
|
||||
NavSection,
|
||||
OwnTab,
|
||||
} from '../shared/routing';
|
||||
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
|
||||
import { confirmDiscardFormChanges, markFormClean, markFormDirty } from '../shared/unsavedChanges';
|
||||
import { SideNav } from './SideNav/SideNav';
|
||||
@@ -13,9 +26,10 @@ import { REQUEST_TYPES } from '../data/requests';
|
||||
import SubmitFeature from '../features/submit/SubmitFeature';
|
||||
import ApproveFeature from '../features/approve/ApproveFeature';
|
||||
import EditFeature from '../features/edit/EditFeature';
|
||||
import { RequestsTableView } from '../features/myRequests/RequestsTableView';
|
||||
import { ListView, RequestsTableView } from '../features/myRequests/RequestsTableView';
|
||||
import { OwnTabs } from '../features/myRequests/OwnTabs';
|
||||
import { SearchView } from '../features/myRequests/SearchView';
|
||||
import { getPendingApproval } from '../features/myRequests/myRequests.service';
|
||||
import { getNavCounts, INavCounts } from '../features/myRequests/myRequests.service';
|
||||
import { IMyRequest } from '../features/myRequests/models/IMyRequest';
|
||||
import { isEditableStatus } from '../shared/approval';
|
||||
|
||||
@@ -26,6 +40,16 @@ import { isEditableStatus } from '../shared/approval';
|
||||
// 'approvals'/'own' — таблицы заявок. Клик по строке таблицы открывает заявку
|
||||
// в выезжающей панели ПОВЕРХ раздела (rpmode=approve&rptype=&rpid=), поэтому
|
||||
// раздел под ней сохраняется; та же ссылка приходит из потока Power Automate.
|
||||
// Разделы, которые показывают таблицу заявок, и соответствующее ей
|
||||
// представление. У 'own' оно зависит ещё и от подвкладки (см. ниже), поэтому
|
||||
// здесь стоит заглушка — важен сам факт наличия ключа.
|
||||
const LIST_VIEWS: Partial<Record<NavSection, ListView>> = {
|
||||
approvals: 'approvals',
|
||||
execution: 'execution',
|
||||
rework: 'rework',
|
||||
own: 'own:created',
|
||||
};
|
||||
|
||||
const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context }) => {
|
||||
const rootClasses = [styles.requestPortal, hasTeamsContext ? styles.teams : '']
|
||||
.filter(Boolean)
|
||||
@@ -33,7 +57,7 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
|
||||
const route = useRoute();
|
||||
const [navCollapsed, setNavCollapsed] = React.useState<boolean>(false);
|
||||
const [approvalsCount, setApprovalsCount] = React.useState<number | undefined>(undefined);
|
||||
const [navCounts, setNavCounts] = React.useState<INavCounts | undefined>(undefined);
|
||||
|
||||
const [activeCategoryId, setActiveCategoryId] = React.useState<string>(() => {
|
||||
if (route.mode === 'create' && route.formKey) {
|
||||
@@ -43,22 +67,21 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
return DEFAULT_CATEGORY_ID;
|
||||
});
|
||||
|
||||
// Счётчик на пункте «На согласовании». Обновляется по тому же событию, что и
|
||||
// сами таблицы (создание заявки, решение согласующего), и на монтировании
|
||||
// (в т.ч. на обновление страницы). Когда сама вкладка «На согласовании»
|
||||
// открыта, RequestsTableView сообщает точное число через onMyTurnCountChange
|
||||
// (ниже) — это перезаписывает значение отсюда, чтобы не было гонки между
|
||||
// двумя независимыми запросами одних и тех же данных.
|
||||
// Счётчики на пунктах-очередях («На согласовании», «На исполнении»,
|
||||
// «Доработка»). Обновляются по тому же событию, что и сами таблицы (создание
|
||||
// заявки, решение согласующего), и на монтировании (в т.ч. на обновление
|
||||
// страницы). Все три приходят из одного вызова getNavCounts — он же следит,
|
||||
// чтобы цифры не разошлись с содержимым вкладок.
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const load = (): void => {
|
||||
getPendingApproval(context)
|
||||
.then((r) => {
|
||||
if (!cancelled) setApprovalsCount(r.totalRows);
|
||||
getNavCounts(context)
|
||||
.then((c) => {
|
||||
if (!cancelled) setNavCounts(c);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setApprovalsCount(undefined);
|
||||
if (!cancelled) setNavCounts(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -100,7 +123,88 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
// таблица на него подписана и перечитает данные фоном, не убирая с экрана
|
||||
// текущие строки (см. RequestsTableView/SearchView).
|
||||
if (nav === route.nav && route.mode === 'submit') emitRequestsChanged();
|
||||
navigate({ mode: 'submit', nav });
|
||||
// Всё восстанавливаемое состояние раздела передаём ЯВНО, из запомненного.
|
||||
// parseRoute подставил бы его и сам, но тогда оно осталось бы только в
|
||||
// состоянии и не попало бы в адрес: ссылка на раздел вела бы на первую
|
||||
// страницу без фильтра, а на экране была бы открыта другая.
|
||||
//
|
||||
// Брать route.ownTab/route.ownStatus здесь нельзя: при переходе ИЗ другого
|
||||
// раздела они undefined (parseRoute заполняет их только при nav==='own'),
|
||||
// и явная передача сбросила бы выбор пользователя.
|
||||
if (nav === 'own') {
|
||||
const ownTab = storedOwnTab();
|
||||
navigate({
|
||||
mode: 'submit',
|
||||
nav,
|
||||
ownTab,
|
||||
ownStatus: storedOwnStatus(ownTab),
|
||||
page: storedPageFor('own', ownTab),
|
||||
query: storedQueryFor('own', ownTab) || undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
navigate({
|
||||
mode: 'submit',
|
||||
nav,
|
||||
page: storedPageFor(nav),
|
||||
query: storedQueryFor(nav) || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const gotoOwnTab = (ownTab: OwnTab): void => {
|
||||
// Фильтр и страница — из запомненного для ЭТОЙ подвкладки (см. goto выше о
|
||||
// том, почему не из route). На вкладках с решениями фильтра нет по смыслу —
|
||||
// storedOwnStatus сам вернёт undefined.
|
||||
persistOwnTab(ownTab);
|
||||
navigate({
|
||||
mode: 'submit',
|
||||
nav: 'own',
|
||||
ownTab,
|
||||
ownStatus: storedOwnStatus(ownTab),
|
||||
page: storedPageFor('own', ownTab),
|
||||
query: storedQueryFor('own', ownTab) || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const setOwnStatus = (ownStatus: string): void => {
|
||||
const normalized = ownStatus || undefined;
|
||||
persistOwnStatus(normalized);
|
||||
// Смена фильтра — другой набор заявок, поэтому явно с первой страницы
|
||||
// (и запоминаем именно её, иначе вернувшись сюда попали бы на старую).
|
||||
persistPage({ mode: 'submit', nav: 'own', ownTab: 'created' }, 1);
|
||||
// Текст поиска переносим ЯВНО: раздел тот же, поле на экране не очищается —
|
||||
// значит и в адресе запрос должен остаться. Без этого rpq пропадал из
|
||||
// ссылки при каждой смене фильтра (в самом состоянии он всё равно
|
||||
// восстанавливался из запомненного, поэтому расхождение адреса с экраном
|
||||
// было видно не сразу).
|
||||
navigate({
|
||||
mode: 'submit',
|
||||
nav: 'own',
|
||||
ownTab: 'created',
|
||||
ownStatus: normalized,
|
||||
page: 1,
|
||||
query: route.query || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
// Смена страницы сохраняет ВЕСЬ остальной маршрут (в т.ч. открытую заявку),
|
||||
// меняется только номер: так «назад» возвращает на прежнюю страницу, а
|
||||
// ссылку со страницей можно передать.
|
||||
const setPage = (page: number): void => {
|
||||
persistPage(route, page);
|
||||
navigate({ ...route, page });
|
||||
};
|
||||
|
||||
// Новый запрос — другой набор результатов, поэтому вместе с ним всегда первая
|
||||
// страница (иначе можно приземлиться на несуществующую).
|
||||
//
|
||||
// replace, а не push: текст уходит сюда после КАЖДОЙ паузы в наборе, и с
|
||||
// обычным переходом каждое слово становилось бы отдельным шагом «назад» —
|
||||
// чтобы уйти со страницы, пришлось бы «отлистать» весь набранный запрос.
|
||||
const setQuery = (query: string): void => {
|
||||
persistQuery(route, query);
|
||||
persistPage(route, 1);
|
||||
navigate({ ...route, query: query || undefined, page: 1 }, { replace: true });
|
||||
};
|
||||
// Клик по строке таблицы. СВОЯ заявка в незавершённом статусе (черновик,
|
||||
// возвращённая, отклонённая) открывается на редактирование, всё остальное —
|
||||
@@ -112,9 +216,25 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
nav: route.nav,
|
||||
formKey: request.formKey,
|
||||
itemId: request.id,
|
||||
ownTab: route.ownTab,
|
||||
ownStatus: route.ownStatus,
|
||||
// Страница и запрос — часть того же списка под панелью: без них ссылка на
|
||||
// открытую заявку вела бы на первую страницу без поиска.
|
||||
page: route.page,
|
||||
query: route.query,
|
||||
});
|
||||
|
||||
const closeDrawer = (): void => navigate({ mode: 'submit', nav: route.nav });
|
||||
// Закрывая панель, возвращаемся ровно в тот список, из которого её открыли —
|
||||
// включая подвкладку и фильтр «Моих заявок».
|
||||
const closeDrawer = (): void =>
|
||||
navigate({
|
||||
mode: 'submit',
|
||||
nav: route.nav,
|
||||
ownTab: route.ownTab,
|
||||
ownStatus: route.ownStatus,
|
||||
page: route.page,
|
||||
query: route.query,
|
||||
});
|
||||
|
||||
// Закрытие панели РЕДАКТИРОВАНИЯ: там открыта форма, и её содержимое живёт
|
||||
// только в памяти — уходя без сохранения, пользователь теряет правки.
|
||||
@@ -132,7 +252,11 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
<SideNav
|
||||
activeId={route.nav}
|
||||
onChange={goto}
|
||||
approvalsCount={approvalsCount}
|
||||
counts={{
|
||||
approvals: navCounts?.approvals,
|
||||
execution: navCounts?.execution,
|
||||
rework: navCounts?.rework,
|
||||
}}
|
||||
collapsed={navCollapsed}
|
||||
onToggleCollapsed={() => setNavCollapsed((v) => !v)}
|
||||
/>
|
||||
@@ -181,16 +305,33 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
{route.nav === 'search' && (
|
||||
<SearchView
|
||||
context={context}
|
||||
page={route.page ?? 1}
|
||||
onPageChange={setPage}
|
||||
query={route.query ?? ''}
|
||||
onQueryChange={setQuery}
|
||||
onOpen={openRequest}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(route.nav === 'approvals' || route.nav === 'own') && (
|
||||
{route.nav === 'own' && (
|
||||
<OwnTabs activeId={route.ownTab ?? 'created'} onChange={gotoOwnTab} />
|
||||
)}
|
||||
|
||||
{LIST_VIEWS[route.nav] !== undefined && (
|
||||
<RequestsTableView
|
||||
context={context}
|
||||
kind={route.nav === 'approvals' ? 'approvals' : 'own'}
|
||||
view={
|
||||
route.nav === 'own'
|
||||
? (`own:${route.ownTab ?? 'created'}` as ListView)
|
||||
: (LIST_VIEWS[route.nav] as ListView)
|
||||
}
|
||||
statusFilter={route.ownStatus ?? ''}
|
||||
onStatusFilterChange={setOwnStatus}
|
||||
page={route.page ?? 1}
|
||||
onPageChange={setPage}
|
||||
query={route.query ?? ''}
|
||||
onQueryChange={setQuery}
|
||||
onOpen={openRequest}
|
||||
onMyTurnCountChange={route.nav === 'approvals' ? setApprovalsCount : undefined}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user