скелет готов
This commit is contained in:
@@ -13,6 +13,8 @@ describe('parseRoute', () => {
|
||||
|
||||
it('rpnav задаёт раздел бокового меню', () => {
|
||||
expect(parseRoute('?rpnav=approvals').nav).toBe('approvals');
|
||||
expect(parseRoute('?rpnav=execution').nav).toBe('execution');
|
||||
expect(parseRoute('?rpnav=rework').nav).toBe('rework');
|
||||
expect(parseRoute('?rpnav=own').nav).toBe('own');
|
||||
expect(parseRoute('?rpnav=search').nav).toBe('search');
|
||||
});
|
||||
@@ -73,6 +75,64 @@ describe('parseRoute', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('подвкладки и фильтр «Моих заявок»', () => {
|
||||
it('без rptab — подвкладка по умолчанию', () => {
|
||||
expect(parseRoute('?rpnav=own').ownTab).toBe('created');
|
||||
});
|
||||
|
||||
it('rptab задаёт подвкладку', () => {
|
||||
expect(parseRoute('?rpnav=own&rptab=approved').ownTab).toBe('approved');
|
||||
expect(parseRoute('?rpnav=own&rptab=rejected').ownTab).toBe('rejected');
|
||||
expect(parseRoute('?rpnav=own&rptab=executed').ownTab).toBe('executed');
|
||||
});
|
||||
|
||||
it('незнакомый rptab откатывается на подвкладку по умолчанию', () => {
|
||||
expect(parseRoute('?rpnav=own&rptab=bogus').ownTab).toBe('created');
|
||||
});
|
||||
|
||||
it('вне «Моих заявок» подвкладка не читается — чужой rptab туда не протекает', () => {
|
||||
expect(parseRoute('?rpnav=approvals&rptab=approved').ownTab).toBeUndefined();
|
||||
expect(parseRoute('?rpnav=search&rptab=approved').ownTab).toBeUndefined();
|
||||
});
|
||||
|
||||
it('фильтр по статусу действует только на вкладке «Инициированные»', () => {
|
||||
expect(parseRoute('?rpnav=own&rpstatus=Черновик').ownStatus).toBe('Черновик');
|
||||
// У вкладок с решениями отбор задаёт сама вкладка — фильтр там бессмыслен.
|
||||
expect(parseRoute('?rpnav=own&rptab=approved&rpstatus=Черновик').ownStatus).toBeUndefined();
|
||||
expect(parseRoute('?rpnav=approvals&rpstatus=Черновик').ownStatus).toBeUndefined();
|
||||
});
|
||||
|
||||
it('значения по умолчанию не засоряют адрес', () => {
|
||||
expect(routeUrl({ mode: 'submit', nav: 'own', ownTab: 'created' })).not.toContain('rptab');
|
||||
expect(routeUrl({ mode: 'submit', nav: 'own' })).not.toContain('rptab');
|
||||
});
|
||||
|
||||
it('непустая подвкладка и фильтр попадают в адрес', () => {
|
||||
expect(routeUrl({ mode: 'submit', nav: 'own', ownTab: 'rejected' })).toContain('rptab=rejected');
|
||||
expect(routeUrl({ mode: 'submit', nav: 'own', ownTab: 'created', ownStatus: 'Черновик' })).toContain('rpstatus=');
|
||||
});
|
||||
|
||||
it('фильтр не пишется в адрес вместе с подвкладкой решений', () => {
|
||||
const url = routeUrl({ mode: 'submit', nav: 'own', ownTab: 'approved', ownStatus: 'Черновик' });
|
||||
expect(url).toContain('rptab=approved');
|
||||
expect(url).not.toContain('rpstatus');
|
||||
});
|
||||
|
||||
it('уход из «Моих заявок» убирает подвкладку и фильтр из адреса', () => {
|
||||
navigate({ mode: 'submit', nav: 'own', ownTab: 'rejected' });
|
||||
navigate({ mode: 'submit', nav: 'approvals' });
|
||||
expect(window.location.search).not.toContain('rptab');
|
||||
expect(window.location.search).not.toContain('rpstatus');
|
||||
});
|
||||
|
||||
it('round-trip для подвкладки решений', () => {
|
||||
navigate({ mode: 'submit', nav: 'own', ownTab: 'executed' });
|
||||
const route = parseRoute(window.location.search);
|
||||
expect(route.nav).toBe('own');
|
||||
expect(route.ownTab).toBe('executed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('routeUrl', () => {
|
||||
it('раздел по умолчанию не засоряет адрес параметром', () => {
|
||||
expect(routeUrl({ mode: 'submit', nav: 'new' })).toBe('/');
|
||||
@@ -103,9 +163,24 @@ describe('navigate', () => {
|
||||
nav: 'own',
|
||||
formKey: 'hire',
|
||||
itemId: 42,
|
||||
// Подвкладка «Моих заявок» всегда определена внутри раздела — в адресе её
|
||||
// нет только потому, что это значение по умолчанию.
|
||||
ownTab: 'created',
|
||||
ownStatus: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('подвкладка и фильтр «Моих заявок» переживают открытие и закрытие заявки', () => {
|
||||
navigate({ mode: 'submit', nav: 'own', ownTab: 'created', ownStatus: 'Черновик' });
|
||||
navigate({ mode: 'approve', nav: 'own', formKey: 'hire', itemId: 42, ownTab: 'created', ownStatus: 'Черновик' });
|
||||
|
||||
const opened = parseRoute(window.location.search);
|
||||
expect(opened.ownStatus).toBe('Черновик');
|
||||
|
||||
navigate({ mode: 'submit', nav: 'own', ownTab: opened.ownTab, ownStatus: opened.ownStatus });
|
||||
expect(parseRoute(window.location.search).ownStatus).toBe('Черновик');
|
||||
});
|
||||
|
||||
it('round-trip для создания производной заявки', () => {
|
||||
navigate({ mode: 'create', nav: 'new', formKey: 'workplace', sourceId: 15 });
|
||||
expect(parseRoute(window.location.search)).toEqual({
|
||||
|
||||
@@ -24,8 +24,25 @@ import * as React from 'react';
|
||||
// отклонённую) на редактирование в той же выезжающей панели, что и просмотр.
|
||||
export type PortalMode = 'submit' | 'approve' | 'create' | 'edit';
|
||||
|
||||
/** Раздел бокового меню. */
|
||||
export type NavSection = 'new' | 'approvals' | 'own' | 'search';
|
||||
/**
|
||||
* Раздел бокового меню.
|
||||
*
|
||||
* Три раздела — очереди действий текущего пользователя, и они НЕ пересекаются
|
||||
* (см. ApprovalQueueKind в myRequests.service.ts):
|
||||
* 'approvals' — ждут его согласования (статус строки «На согласовании»),
|
||||
* 'execution' — ждут его исполнения (статус строки «На исполнении»),
|
||||
* 'rework' — ЕГО заявки, которые вернули ему на доработку (статус ЗАЯВКИ
|
||||
* «Возвращена на доработку»); в отличие от первых двух смотрит
|
||||
* на список данных по автору, а не на строки маршрута.
|
||||
*/
|
||||
export type NavSection = 'new' | 'approvals' | 'execution' | 'rework' | 'own' | 'search';
|
||||
|
||||
/**
|
||||
* Подвкладка раздела «Мои заявки». 'created' — заявки, где пользователь автор
|
||||
* (единственная, где есть фильтр по статусу); остальные — история его решений
|
||||
* как участника чужих маршрутов.
|
||||
*/
|
||||
export type OwnTab = 'created' | 'approved' | 'rejected' | 'executed';
|
||||
|
||||
export interface IRoute {
|
||||
mode: PortalMode;
|
||||
@@ -37,15 +54,32 @@ export interface IRoute {
|
||||
itemId?: number;
|
||||
/** Id исходной заявки для предзаполнения — для 'create' (напр. орг. места на основе прием). */
|
||||
sourceId?: number;
|
||||
/** Подвкладка «Моих заявок»; задана только при nav='own'. */
|
||||
ownTab?: OwnTab;
|
||||
/** Фильтр по статусу заявки; задан только при nav='own' и ownTab='created'. */
|
||||
ownStatus?: string;
|
||||
}
|
||||
|
||||
const ROUTE_CHANGE_EVENT = 'rp-route-change';
|
||||
|
||||
function parseNav(raw: string | undefined): NavSection {
|
||||
if (raw === 'approvals' || raw === 'own' || raw === 'search') return raw;
|
||||
if (
|
||||
raw === 'approvals' ||
|
||||
raw === 'execution' ||
|
||||
raw === 'rework' ||
|
||||
raw === 'own' ||
|
||||
raw === 'search'
|
||||
) {
|
||||
return raw;
|
||||
}
|
||||
return 'new';
|
||||
}
|
||||
|
||||
function parseOwnTab(raw: string | undefined): OwnTab {
|
||||
if (raw === 'approved' || raw === 'rejected' || raw === 'executed') return raw;
|
||||
return 'created';
|
||||
}
|
||||
|
||||
/** Разобрать строку запроса (по умолчанию — из адреса страницы) в маршрут. */
|
||||
export function parseRoute(search: string = window.location.search): IRoute {
|
||||
const params = new URLSearchParams(search);
|
||||
@@ -57,19 +91,28 @@ export function parseRoute(search: string = window.location.search): IRoute {
|
||||
const sourceRaw = params.get('rpsource');
|
||||
const sourceId = sourceRaw ? Number(sourceRaw) : undefined;
|
||||
|
||||
// Подвкладка и фильтр — состояние раздела «Мои заявки», поэтому вне его не
|
||||
// читаются вовсе (иначе чужой rptab из ссылки «протёк» бы в другой раздел).
|
||||
// Фильтр по статусу живёт только на вкладке «Инициированные»: у вкладок с
|
||||
// решениями отбор задаёт сама вкладка.
|
||||
const ownTab = nav === 'own' ? parseOwnTab(params.get('rptab') ?? undefined) : undefined;
|
||||
const ownStatus = ownTab === 'created' ? params.get('rpstatus') ?? undefined : undefined;
|
||||
|
||||
// Открытая заявка (панель поверх раздела) — при полном наборе параметров.
|
||||
// Раздел под панелью сохраняется целиком, вместе с подвкладкой и фильтром:
|
||||
// закрыв панель, пользователь должен вернуться ровно в тот же список.
|
||||
if (mode === 'approve' && formKey && itemId && !isNaN(itemId)) {
|
||||
return { mode: 'approve', nav, formKey, itemId };
|
||||
return { mode: 'approve', nav, formKey, itemId, ownTab, ownStatus };
|
||||
}
|
||||
// Редактирование собственной незавершённой заявки — та же панель, но с формой.
|
||||
if (mode === 'edit' && formKey && itemId && !isNaN(itemId)) {
|
||||
return { mode: 'edit', nav, formKey, itemId };
|
||||
return { mode: 'edit', nav, formKey, itemId, ownTab, ownStatus };
|
||||
}
|
||||
// Создание производной заявки с предзаполнением из исходной.
|
||||
if (formKey && sourceId && !isNaN(sourceId)) {
|
||||
return { mode: 'create', nav: 'new', formKey, sourceId };
|
||||
}
|
||||
return { mode: 'submit', nav };
|
||||
return { mode: 'submit', nav, ownTab, ownStatus };
|
||||
}
|
||||
|
||||
// Собрать query-строку маршрута, сохранив посторонние параметры страницы.
|
||||
@@ -80,10 +123,19 @@ function buildQuery(route: IRoute): string {
|
||||
params.delete('rptype');
|
||||
params.delete('rpid');
|
||||
params.delete('rpsource');
|
||||
params.delete('rptab');
|
||||
params.delete('rpstatus');
|
||||
|
||||
if (route.nav !== 'new') {
|
||||
params.set('rpnav', route.nav);
|
||||
}
|
||||
// Значения по умолчанию в адрес не пишем — чтобы обычный переход на «Мои
|
||||
// заявки» давал чистую ссылку, а не ?rpnav=own&rptab=created.
|
||||
if (route.nav === 'own') {
|
||||
const ownTab = route.ownTab ?? 'created';
|
||||
if (ownTab !== 'created') params.set('rptab', ownTab);
|
||||
else if (route.ownStatus) params.set('rpstatus', route.ownStatus);
|
||||
}
|
||||
if ((route.mode === 'approve' || route.mode === 'edit') && route.formKey && route.itemId) {
|
||||
params.set('rpmode', route.mode);
|
||||
params.set('rptype', route.formKey);
|
||||
|
||||
Reference in New Issue
Block a user