From fac11f3b6bae904d2c46e164c09aa28c8750603e Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 25 Sep 2026 14:46:12 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=20=C2=AB=D0=92?= =?UTF-8?q?=D1=81=D0=B5=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B8=C2=BB=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9=20=D1=81=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=83=D0=BF=D0=BE=D0=BC=20=D0=BA=20=D1=82=D0=B8?= =?UTF-8?q?=D0=BF=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новый раздел бокового меню показывает заявки выбранного типа целиком (все авторы, а не только свои) — виден только тем, кому в админке approval-route выдан RequestTypeAccess хотя бы на один тип. На форме подачи заявки появляется кнопка «Все заявки этого типа», если доступ выдан именно на открытый тип. Co-Authored-By: Claude Sonnet 5 --- .../components/RequestPortal.tsx | 66 ++++++++++- .../myRequests/AllRequestsFeature.module.scss | 14 +++ .../myRequests/AllRequestsFeature.tsx | 103 ++++++++++++++++++ .../features/myRequests/RequestsTableView.tsx | 27 ++++- .../features/myRequests/models/IMyRequest.ts | 10 +- .../features/myRequests/myRequests.service.ts | 47 +++++++- .../features/submit/SubmitFeature.module.scss | 26 +++++ .../features/submit/SubmitFeature.tsx | 76 ++++++++++--- .../shared/approval/approvalRoute.service.ts | 22 ++++ .../requestPortal/shared/approval/index.ts | 2 + .../shared/approval/requestTypeAccess.ts | 48 ++++++++ src/webparts/requestPortal/shared/routing.ts | 27 ++++- 12 files changed, 438 insertions(+), 30 deletions(-) create mode 100644 src/webparts/requestPortal/features/myRequests/AllRequestsFeature.module.scss create mode 100644 src/webparts/requestPortal/features/myRequests/AllRequestsFeature.tsx create mode 100644 src/webparts/requestPortal/shared/approval/requestTypeAccess.ts diff --git a/src/webparts/requestPortal/components/RequestPortal.tsx b/src/webparts/requestPortal/components/RequestPortal.tsx index 54995fc..6cef99a 100644 --- a/src/webparts/requestPortal/components/RequestPortal.tsx +++ b/src/webparts/requestPortal/components/RequestPortal.tsx @@ -34,11 +34,13 @@ import EditFeature from '../features/edit/EditFeature'; import { ListView, RequestsTableView } from '../features/myRequests/RequestsTableView'; import { OwnTabs } from '../features/myRequests/OwnTabs'; import { SearchView } from '../features/myRequests/SearchView'; -import { getNavCounts, INavCounts } from '../features/myRequests/myRequests.service'; +import { getLinkedTypes, getNavCounts, INavCounts } from '../features/myRequests/myRequests.service'; import { IMyRequest } from '../features/myRequests/models/IMyRequest'; import { isEditableStatus } from '../shared/approval'; import { hasReportsAccess } from '../shared/reports/reportsAccess'; import { ReportsFeature } from '../features/reports/ReportsFeature'; +import { AllRequestsFeature } from '../features/myRequests/AllRequestsFeature'; +import { accessibleAllRequestsTypes } from '../shared/approval'; // Оболочка портала: тёмное боковое меню (разделы) + основная область. // Логики фич тут нет — файл общий, меняется редко. @@ -102,6 +104,33 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context } }, [route.nav, canSeeReports]); + // Раздел «Все заявки» — виден только тем, кому в админке approval-route + // выдан RequestTypeAccess хотя бы на один тип (см. shared/approval/ + // requestTypeAccess.ts). undefined — ответ ещё не пришёл. + const [canSeeAllRequests, setCanSeeAllRequests] = React.useState(undefined); + React.useEffect(() => { + let cancelled = false; + setCanSeeAllRequests(undefined); + getLinkedTypes(context) + .then((types) => accessibleAllRequestsTypes(context, types)) + .then((accessible) => { + if (!cancelled) setCanSeeAllRequests(accessible.length > 0); + }) + .catch(() => { + if (!cancelled) setCanSeeAllRequests(false); + }); + return () => { + cancelled = true; + }; + }, [context, impersonatedEmail]); + + // Та же защита от прямой ссылки, что и у «Отчётов» выше. + React.useEffect(() => { + if (route.nav === 'allRequests' && canSeeAllRequests === false) { + navigate({ mode: 'submit', nav: 'new' }, { replace: true }); + } + }, [route.nav, canSeeAllRequests]); + // Уникальный код заявки для заголовка Drawer (approve/edit) — сама форма // узнаёт его только после загрузки (см. onCodeLoaded в ApproveFeature/ // EditFeature), поэтому не берём route.itemId (это RowID в SharePoint, а не @@ -224,6 +253,24 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context }); }; + // Переход в «Все заявки» — либо с бокового меню (typeId не задан, раздел сам + // выберет тип), либо с кнопки формы (typeId уже известен, см. + // gotoAllRequestsForType и SubmitFeature.onOpenAllRequests). + const gotoAllRequestsType = (typeId: number): void => { + navigate({ + mode: 'submit', + nav: 'allRequests', + allTypeId: typeId, + page: storedPageFor('allRequests'), + query: storedQueryFor('allRequests') || undefined, + }); + }; + + const gotoAllRequestsForType = (typeId: number): void => { + persistNav('allRequests'); + gotoAllRequestsType(typeId); + }; + const setOwnStatus = (ownStatus: string): void => { const normalized = ownStatus || undefined; persistOwnStatus(normalized); @@ -281,6 +328,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context listId: request.listId, ownTab: route.ownTab, ownStatus: route.ownStatus, + allTypeId: route.allTypeId, // Страница и запрос — часть того же списка под панелью: без них ссылка на // открытую заявку вела бы на первую страницу без поиска. page: route.page, @@ -295,6 +343,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context nav: route.nav, ownTab: route.ownTab, ownStatus: route.ownStatus, + allTypeId: route.allTypeId, page: route.page, query: route.query, }); @@ -324,6 +373,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context { id: 'rework', title: 'Доработка', icon: 'edit', count: navCounts?.rework }, { id: 'own', title: 'Мои заявки', icon: 'clipboard' }, { id: 'search', title: 'Поиск', icon: 'search' }, + ...(canSeeAllRequests ? [{ id: 'allRequests', title: 'Все заявки', icon: 'people' } as ISidebarItem] : []), ...(canSeeReports ? [{ id: 'reports', title: 'Отчёты', icon: 'chart' } as ISidebarItem] : []), { id: 'instructions', title: 'Инструкция пользователя', icon: 'info' }, ]; @@ -413,6 +463,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context : undefined } onFormSelected={(formKey) => navigate({ mode: 'create', nav: 'new', formKey })} + onOpenAllRequests={gotoAllRequestsForType} /> @@ -433,6 +484,19 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context {route.nav === 'reports' && canSeeReports && } + {route.nav === 'allRequests' && canSeeAllRequests && ( + + )} + {LIST_VIEWS[route.nav] !== undefined && ( void; + page: number; + onPageChange: (page: number) => void; + query: string; + onQueryChange: (query: string) => void; + onOpen: (request: IMyRequest) => void; +} + +/** + * Раздел «Все заявки»: доступен только тем, у кого в админке approval-route + * настроен RequestTypeAccess хотя бы на один тип заявки (см. + * shared/approval/requestTypeAccess.ts). Показывает заявки ВСЕХ авторов + * выбранного типа — в отличие от «Моих заявок», где отбор всегда идёт по + * текущему пользователю. + */ +export const AllRequestsFeature: React.FC = ({ + context, + selectedTypeId, + onTypeChange, + page, + onPageChange, + query, + onQueryChange, + onOpen, +}) => { + const [types, setTypes] = React.useState(undefined); + const [error, setError] = React.useState(''); + + React.useEffect(() => { + let cancelled = false; + getLinkedTypes(context) + .then((all) => accessibleAllRequestsTypes(context, all)) + .then((accessible) => { + if (!cancelled) setTypes(accessible); + }) + .catch((e) => { + if (!cancelled) setError((e as Error).message); + }); + return () => { + cancelled = true; + }; + }, [context]); + + // Тип по умолчанию — из адреса, если он всё ещё доступен, иначе первый + // доступный. Отдельным эффектом (а не при рендере): смена выбора идёт через + // navigate() у вызывающей стороны, и до его отработки selectedTypeId ещё + // хранит предыдущее значение. + React.useEffect(() => { + if (!types || types.length === 0) return; + const stillValid = !!selectedTypeId && types.some((t) => t.id === selectedTypeId); + if (!stillValid) onTypeChange(types[0].id); + // eslint-disable-next-line react-hooks/exhaustive-deps -- onTypeChange стабилен по смыслу (см. gotoAllRequestsType в RequestPortal), включение его в deps дало бы лишний повторный запуск на каждый рендер + }, [types, selectedTypeId]); + + if (error) { + return Не удалось загрузить типы заявок: {error}; + } + if (types === undefined) { + return
Загрузка…
; + } + if (types.length === 0) { + return
Вам не выдан доступ ни к одному типу заявок.
; + } + + const selectedType = types.find((t) => t.id === selectedTypeId) ?? types[0]; + + return ( +
+ {types.length > 1 && ( +
+