Параллельные шаги (#3)

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/3
This commit was merged in pull request #3.
This commit is contained in:
2026-08-06 12:36:22 +00:00
parent 40f9ac6c03
commit df9d12622d
24 changed files with 967 additions and 175 deletions
@@ -4,6 +4,7 @@ import type { IRequestPortalProps } from './IRequestPortalProps';
import { IconSprite } from '../shared/ui/Icon/IconSprite';
import { navigate, useRoute, NavSection } from '../shared/routing';
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
import { confirmDiscardFormChanges, markFormClean, markFormDirty } from '../shared/unsavedChanges';
import { SideNav } from './SideNav/SideNav';
import { Drawer } from './Drawer/Drawer';
import { CategoryTabs } from './CategoryTabs/CategoryTabs';
@@ -11,9 +12,12 @@ import { CATEGORIES, DEFAULT_CATEGORY_ID } from '../data/categories';
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 { SearchView } from '../features/myRequests/SearchView';
import { getPendingApproval } from '../features/myRequests/myRequests.service';
import { IMyRequest } from '../features/myRequests/models/IMyRequest';
import { isEditableStatus } from '../shared/approval';
// Оболочка портала: тёмное боковое меню (разделы) + основная область.
// Логики фич тут нет — файл общий, меняется редко.
@@ -68,13 +72,28 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
// Переход «создать на основе» (в т.ч. из открытой заявки в панели): переключаем
// вкладку категорий на категорию целевой формы, чтобы она совпала с открытой формой.
// Форма при этом пересоздаётся (см. key у SubmitFeature), поэтому прежние
// несохранённые правки к ней уже не относятся — снимаем флаг.
React.useEffect(() => {
if (route.mode === 'create' && route.formKey) {
const found = REQUEST_TYPES.filter((r) => r.id === route.formKey)[0];
if (found) setActiveCategoryId(found.categoryId);
markFormClean();
}
}, [route.mode, route.formKey]);
// Сохранение (в том числе черновика) шлёт rp-requests-changed — после него
// терять нечего, предупреждать при «создать на основе» больше не о чем.
React.useEffect(() => onRequestsChanged(markFormClean), []);
// Смена категории закрывает открытую форму (SubmitFeature сбрасывает выбор,
// когда категория перестаёт совпадать) — значит введённое пропадёт.
const changeCategory = (categoryId: string): void => {
if (!confirmDiscardFormChanges()) return;
markFormClean();
setActiveCategoryId(categoryId);
};
const goto = (nav: NavSection): void => {
// Клик по УЖЕ активному пункту меню не меняет маршрут, значит эффект
// загрузки в таблице не перезапустится сам. Шлём событие обновления —
@@ -83,8 +102,28 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
if (nav === route.nav && route.mode === 'submit') emitRequestsChanged();
navigate({ mode: 'submit', nav });
};
// Клик по строке таблицы. СВОЯ заявка в незавершённом статусе (черновик,
// возвращённая, отклонённая) открывается на редактирование, всё остальное —
// на просмотр: чужую заявку править нельзя, а свою уже ушедшую по маршруту —
// тем более (её статус в isEditableStatus не входит).
const openRequest = (request: IMyRequest): void =>
navigate({
mode: request.kind === 'own' && isEditableStatus(request.status) ? 'edit' : 'approve',
nav: route.nav,
formKey: request.formKey,
itemId: request.id,
});
const closeDrawer = (): void => navigate({ mode: 'submit', nav: route.nav });
// Закрытие панели РЕДАКТИРОВАНИЯ: там открыта форма, и её содержимое живёт
// только в памяти — уходя без сохранения, пользователь теряет правки.
const closeEditDrawer = (): void => {
if (!confirmDiscardFormChanges()) return;
markFormClean();
closeDrawer();
};
return (
<section className={rootClasses}>
<IconSprite />
@@ -100,49 +139,57 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
</div>
<main className={styles.main}>
{route.nav === 'new' ? (
<>
<div className={styles.tabsWrap}>
<CategoryTabs
categories={CATEGORIES}
activeId={activeCategoryId}
onChange={setActiveCategoryId}
/>
</div>
<SubmitFeature
context={context}
activeCategoryId={activeCategoryId}
initialForm={
route.mode === 'create' && route.formKey && route.sourceId
? { formKey: route.formKey, sourceId: route.sourceId }
: undefined
}
{/* Раздел «Новая заявка» держим СМОНТИРОВАННЫМ и просто прячем стилем,
когда открыта другая вкладка. Иначе React размонтировал бы форму, и
всё введённое пропадало бы при переключении на «Мои заявки» и
обратно. Ключ завязан на «создать на основе»: обычные переключения
вкладок его не меняют (состояние живёт), а вот открытие новой
заявки на основе исходной пересоздаёт форму — SubmitFeature читает
initialForm только при монтировании. */}
<div
className={route.nav === 'new' ? undefined : styles.hiddenPane}
// Делегирование: любой пользовательский ввод внутри раздела помечает
// форму изменённой. Так не нужно инструментировать каждую форму по
// отдельности, и программное заполнение (предзаполнение «на основе»)
// сюда не попадает — React выставляет значения без этих событий.
onInput={markFormDirty}
onChange={markFormDirty}
>
<div className={styles.tabsWrap}>
<CategoryTabs
categories={CATEGORIES}
activeId={activeCategoryId}
onChange={changeCategory}
/>
</>
) : route.nav === 'search' ? (
<SearchView
</div>
<SubmitFeature
key={
route.mode === 'create' && route.formKey && route.sourceId
? `create-${route.formKey}-${route.sourceId}`
: 'blank'
}
context={context}
onOpen={(request) =>
navigate({
mode: 'approve',
nav: route.nav,
formKey: request.formKey,
itemId: request.id,
})
activeCategoryId={activeCategoryId}
initialForm={
route.mode === 'create' && route.formKey && route.sourceId
? { formKey: route.formKey, sourceId: route.sourceId }
: undefined
}
/>
) : (
</div>
{route.nav === 'search' && (
<SearchView
context={context}
onOpen={openRequest}
/>
)}
{(route.nav === 'approvals' || route.nav === 'own') && (
<RequestsTableView
context={context}
kind={route.nav === 'approvals' ? 'approvals' : 'own'}
onOpen={(request) =>
navigate({
mode: 'approve',
nav: route.nav,
formKey: request.formKey,
itemId: request.id,
})
}
onOpen={openRequest}
onMyTurnCountChange={route.nav === 'approvals' ? setApprovalsCount : undefined}
/>
)}
@@ -154,6 +201,21 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
<ApproveFeature context={context} formKey={route.formKey} itemId={route.itemId} />
</Drawer>
)}
{/* Своя незавершённая заявка — та же панель, но с формой. Ввод здесь тоже
помечает состояние изменённым, поэтому обёртка ловит input/change. */}
{route.mode === 'edit' && route.formKey && route.itemId && (
<Drawer title={`Заявка №${route.itemId}`} onClose={closeEditDrawer}>
<div onInput={markFormDirty} onChange={markFormDirty}>
<EditFeature
context={context}
formKey={route.formKey}
itemId={route.itemId}
onClose={closeDrawer}
/>
</div>
</Drawer>
)}
</section>
);
};