Feature/request type access all requests (#22)
Tests / frontend-test (push) Successful in 1m24s
Tests / frontend-test (push) Successful in 1m24s
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/22
This commit was merged in pull request #22.
This commit is contained in:
@@ -6,6 +6,34 @@
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.tileWrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.allRequestsButton {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: $r-md;
|
||||
background: $white;
|
||||
color: $gray-400;
|
||||
cursor: pointer;
|
||||
transition: color .18s, border-color .18s;
|
||||
|
||||
&:hover {
|
||||
color: $gold-600;
|
||||
border-color: $gold-400;
|
||||
}
|
||||
}
|
||||
|
||||
.tile {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
@@ -7,9 +7,18 @@ import { useIsTester } from '../../shared/useIsTester';
|
||||
interface IRequestGridProps {
|
||||
requests: IRequestType[];
|
||||
onSelect: (request: IRequestType) => void;
|
||||
/** pageUrl → RequestTypeId, только для типов с доступом к «Все заявки» (см. useAccessibleAllRequestsFormKeys). */
|
||||
accessibleAllRequestsIds?: Map<string, number>;
|
||||
/** Клик по кнопке «Все заявки» на плитке — задан вместе с accessibleAllRequestsIds. */
|
||||
onOpenAllRequests?: (requestTypeId: number) => void;
|
||||
}
|
||||
|
||||
export const RequestGrid: React.FC<IRequestGridProps> = ({ requests, onSelect }) => {
|
||||
export const RequestGrid: React.FC<IRequestGridProps> = ({
|
||||
requests,
|
||||
onSelect,
|
||||
accessibleAllRequestsIds,
|
||||
onOpenAllRequests,
|
||||
}) => {
|
||||
// Группа «Тестирование» видит формы «в разработке» открытыми — чтобы проверять
|
||||
// их до запуска для всех (см. shared/useIsTester).
|
||||
const isTester = useIsTester() === true;
|
||||
@@ -22,33 +31,45 @@ export const RequestGrid: React.FC<IRequestGridProps> = ({ requests, onSelect })
|
||||
<div className={styles.grid}>
|
||||
{requests.map((request) => {
|
||||
const isLocked = !!request.comingSoon && !isTester;
|
||||
const allRequestsTypeId = accessibleAllRequestsIds?.get(request.id);
|
||||
return (
|
||||
<button
|
||||
key={request.id}
|
||||
type="button"
|
||||
className={isLocked ? `${styles.tile} ${styles.tileDisabled}` : styles.tile}
|
||||
disabled={isLocked}
|
||||
onClick={() => onSelect(request)}
|
||||
>
|
||||
<span className={styles.tileIcon}>
|
||||
<Icon name={request.icon} />
|
||||
</span>
|
||||
<span className={styles.tileBody}>
|
||||
<span className={styles.tileTitle}>{request.title}</span>
|
||||
{request.comingSoon ? (
|
||||
<Badge variant="muted" size="sm">
|
||||
В разработке
|
||||
</Badge>
|
||||
) : (
|
||||
request.description && <span className={styles.tileDesc}>{request.description}</span>
|
||||
)}
|
||||
</span>
|
||||
{!isLocked && (
|
||||
<span className={styles.tileArrow}>
|
||||
<Icon name="chevron-right" size="sm" />
|
||||
<div key={request.id} className={styles.tileWrap}>
|
||||
<button
|
||||
type="button"
|
||||
className={isLocked ? `${styles.tile} ${styles.tileDisabled}` : styles.tile}
|
||||
disabled={isLocked}
|
||||
onClick={() => onSelect(request)}
|
||||
>
|
||||
<span className={styles.tileIcon}>
|
||||
<Icon name={request.icon} />
|
||||
</span>
|
||||
<span className={styles.tileBody}>
|
||||
<span className={styles.tileTitle}>{request.title}</span>
|
||||
{request.comingSoon ? (
|
||||
<Badge variant="muted" size="sm">
|
||||
В разработке
|
||||
</Badge>
|
||||
) : (
|
||||
request.description && <span className={styles.tileDesc}>{request.description}</span>
|
||||
)}
|
||||
</span>
|
||||
{!isLocked && (
|
||||
<span className={styles.tileArrow}>
|
||||
<Icon name="chevron-right" size="sm" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{allRequestsTypeId !== undefined && onOpenAllRequests && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.allRequestsButton}
|
||||
title="Все заявки этого типа"
|
||||
onClick={() => onOpenAllRequests(allRequestsTypeId)}
|
||||
>
|
||||
<Icon name="people" size="sm" />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,13 @@ import type { ISidebarItem } from '@tnsu/ui-kit';
|
||||
import { ConfirmHost } from '../shared/ui';
|
||||
import {
|
||||
navigate,
|
||||
persistAllTypeId,
|
||||
persistNav,
|
||||
persistOwnTab,
|
||||
persistOwnStatus,
|
||||
persistPage,
|
||||
persistQuery,
|
||||
storedAllTypeId,
|
||||
storedOwnStatus,
|
||||
storedOwnTab,
|
||||
storedPageFor,
|
||||
@@ -34,11 +36,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 +106,33 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
}
|
||||
}, [route.nav, canSeeReports]);
|
||||
|
||||
// Раздел «Все заявки» — виден только тем, кому в админке approval-route
|
||||
// выдан RequestTypeAccess хотя бы на один тип (см. shared/approval/
|
||||
// requestTypeAccess.ts). undefined — ответ ещё не пришёл.
|
||||
const [canSeeAllRequests, setCanSeeAllRequests] = React.useState<boolean | undefined>(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, а не
|
||||
@@ -201,6 +232,19 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Тип заявки в «Все заявки» — из запомненного (см. gotoAllRequestsType
|
||||
// ниже, откуда он и запоминается). Не задан ни разу — undefined, раздел
|
||||
// сам выберет первый доступный тип (см. AllRequestsFeature).
|
||||
if (nav === 'allRequests') {
|
||||
navigate({
|
||||
mode: 'submit',
|
||||
nav,
|
||||
allTypeId: storedAllTypeId(),
|
||||
page: storedPageFor(nav),
|
||||
query: storedQueryFor(nav) || undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
navigate({
|
||||
mode: 'submit',
|
||||
nav,
|
||||
@@ -224,6 +268,28 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
});
|
||||
};
|
||||
|
||||
// Переход в «Все заявки» — либо с бокового меню (typeId не задан, раздел сам
|
||||
// выберет тип), либо с кнопки формы (typeId уже известен, см.
|
||||
// gotoAllRequestsForType и SubmitFeature.onOpenAllRequests), либо смена типа
|
||||
// в самом разделе (пикер в AllRequestsFeature). Запоминаем выбор — как и
|
||||
// persistOwnTab, только при явном выборе (в т.ч. том, что раздел сделал
|
||||
// сам по умолчанию), чтобы следующий заход открыл тот же тип.
|
||||
const gotoAllRequestsType = (typeId: number): void => {
|
||||
persistAllTypeId(typeId);
|
||||
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 +347,7 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
listId: request.listId,
|
||||
ownTab: route.ownTab,
|
||||
ownStatus: route.ownStatus,
|
||||
allTypeId: route.allTypeId,
|
||||
// Страница и запрос — часть того же списка под панелью: без них ссылка на
|
||||
// открытую заявку вела бы на первую страницу без поиска.
|
||||
page: route.page,
|
||||
@@ -295,6 +362,7 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
nav: route.nav,
|
||||
ownTab: route.ownTab,
|
||||
ownStatus: route.ownStatus,
|
||||
allTypeId: route.allTypeId,
|
||||
page: route.page,
|
||||
query: route.query,
|
||||
});
|
||||
@@ -324,6 +392,7 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ 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 +482,7 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
: undefined
|
||||
}
|
||||
onFormSelected={(formKey) => navigate({ mode: 'create', nav: 'new', formKey })}
|
||||
onOpenAllRequests={gotoAllRequestsForType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -433,6 +503,19 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
|
||||
{route.nav === 'reports' && canSeeReports && <ReportsFeature context={context} />}
|
||||
|
||||
{route.nav === 'allRequests' && canSeeAllRequests && (
|
||||
<AllRequestsFeature
|
||||
context={context}
|
||||
selectedTypeId={route.allTypeId}
|
||||
onTypeChange={gotoAllRequestsType}
|
||||
page={route.page ?? 1}
|
||||
onPageChange={setPage}
|
||||
query={route.query ?? ''}
|
||||
onQueryChange={setQuery}
|
||||
onOpen={openRequest}
|
||||
/>
|
||||
)}
|
||||
|
||||
{LIST_VIEWS[route.nav] !== undefined && (
|
||||
<RequestsTableView
|
||||
context={context}
|
||||
|
||||
Reference in New Issue
Block a user