куча доработок
This commit is contained in:
@@ -12,6 +12,16 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Компактный вариант — для ячейки таблицы (RequestsTableView/SearchView),
|
||||
// повторяет размеры, что были у их собственной .status до объединения.
|
||||
.sm {
|
||||
padding: 3px 10px;
|
||||
border-radius: $r-md;
|
||||
font-size: $fs-caption;
|
||||
font-weight: 600;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.draft {
|
||||
background: $gray-100;
|
||||
color: $gray-700;
|
||||
|
||||
@@ -16,7 +16,25 @@ const VARIANT: Record<string, string> = {
|
||||
[REQUEST_STATUS.closed]: styles.closed,
|
||||
};
|
||||
|
||||
/** Цветная плашка статуса заявки (шапка формы / просмотра). */
|
||||
export const StatusBadge: React.FC<{ status: string }> = ({ status }) => (
|
||||
<span className={`${styles.badge} ${VARIANT[status] || styles.draft}`}>{status}</span>
|
||||
export interface IStatusBadgeProps {
|
||||
status: string;
|
||||
/**
|
||||
* 'md' (по умолчанию) — шапка формы/просмотра заявки; 'sm' — компактный
|
||||
* вариант для ячейки таблицы (RequestsTableView/SearchView). Цвет статуса
|
||||
* не зависит от размера — единственный источник цвета: VARIANT выше.
|
||||
*/
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
/**
|
||||
* Цветная плашка статуса заявки — ЕДИНСТВЕННОЕ место, где статус превращается
|
||||
* в цвет. У таблиц раньше была своя копия этой раскраски (statusClass в
|
||||
* RequestsTableView.tsx/SearchView.tsx) — она разошлась с этой (на согласовании
|
||||
* и возвращена на доработку оказались перепутаны местами), поэтому обе копии
|
||||
* убраны в пользу общего компонента.
|
||||
*/
|
||||
export const StatusBadge: React.FC<IStatusBadgeProps> = ({ status, size = 'md' }) => (
|
||||
<span className={`${styles.badge} ${size === 'sm' ? styles.sm : ''} ${VARIANT[status] || styles.draft}`}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { IRoute, navigate, parseRoute, routeUrl } from './routing';
|
||||
import { IRoute, navigate, parseRoute, persistOwnSelection, routeUrl } from './routing';
|
||||
|
||||
// buildQuery/navigate читают и пишут window.location через history API —
|
||||
// сбрасываем адрес перед каждым тестом, чтобы они не зависели от порядка запуска.
|
||||
// buildQuery/navigate читают и пишут window.location через history API, а
|
||||
// подвкладка/фильтр «Моих заявок» — ещё и localStorage (см. persistOwnSelection) —
|
||||
// сбрасываем оба перед каждым тестом, чтобы они не зависели от порядка запуска.
|
||||
beforeEach(() => {
|
||||
window.history.replaceState({}, '', '/');
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
describe('parseRoute', () => {
|
||||
@@ -124,6 +126,53 @@ describe('подвкладки и фильтр «Моих заявок»', () =>
|
||||
expect(window.location.search).not.toContain('rptab');
|
||||
expect(window.location.search).not.toContain('rpstatus');
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistOwnSelection — запоминание подвкладки/фильтра между визитами', () => {
|
||||
it('без явного rptab в адресе подставляется запомненная подвкладка', () => {
|
||||
persistOwnSelection('rejected', undefined);
|
||||
expect(parseRoute('?rpnav=own').ownTab).toBe('rejected');
|
||||
});
|
||||
|
||||
it('без явного rpstatus подставляется запомненный фильтр (только на «Инициированных»)', () => {
|
||||
persistOwnSelection('created', 'Черновик');
|
||||
expect(parseRoute('?rpnav=own').ownStatus).toBe('Черновик');
|
||||
// На вкладках с решениями запомненный фильтр ни при чём — там его нет по смыслу.
|
||||
persistOwnSelection('approved', 'Черновик');
|
||||
expect(parseRoute('?rpnav=own').ownStatus).toBeUndefined();
|
||||
});
|
||||
|
||||
it('явный параметр в адресе имеет приоритет над запомненным значением', () => {
|
||||
persistOwnSelection('rejected', undefined);
|
||||
expect(parseRoute('?rpnav=own&rptab=executed').ownTab).toBe('executed');
|
||||
});
|
||||
|
||||
it('очистка фильтра persistOwnSelection убирает его и из последующих parseRoute', () => {
|
||||
persistOwnSelection('created', 'Черновик');
|
||||
expect(parseRoute('?rpnav=own').ownStatus).toBe('Черновик');
|
||||
|
||||
persistOwnSelection('created', undefined);
|
||||
expect(parseRoute('?rpnav=own').ownStatus).toBeUndefined();
|
||||
});
|
||||
|
||||
it('запомненное значение не протекает за пределы раздела «Мои заявки»', () => {
|
||||
persistOwnSelection('rejected', undefined);
|
||||
expect(parseRoute('?rpnav=approvals').ownTab).toBeUndefined();
|
||||
});
|
||||
|
||||
it('localStorage недоступен (например, приватный режим) — не роняет parseRoute', () => {
|
||||
const original = window.localStorage.getItem;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- имитация приватного режима, где чтение localStorage бросает
|
||||
(window.localStorage as any).getItem = (): never => {
|
||||
throw new Error('SecurityError');
|
||||
};
|
||||
try {
|
||||
expect(() => parseRoute('?rpnav=own')).not.toThrow();
|
||||
expect(parseRoute('?rpnav=own').ownTab).toBe('created');
|
||||
} finally {
|
||||
window.localStorage.getItem = original;
|
||||
}
|
||||
});
|
||||
|
||||
it('round-trip для подвкладки решений', () => {
|
||||
navigate({ mode: 'submit', nav: 'own', ownTab: 'executed' });
|
||||
|
||||
@@ -62,6 +62,54 @@ export interface IRoute {
|
||||
|
||||
const ROUTE_CHANGE_EVENT = 'rp-route-change';
|
||||
|
||||
// Подвкладка и фильтр «Моих заявок» переживают перезагрузку страницы и новый
|
||||
// визит (localStorage), но НЕ более того — как и текст поиска в
|
||||
// usePersistedQuery.ts, это просто «продолжить с того же места», а не
|
||||
// синхронизация между вкладками/устройствами.
|
||||
//
|
||||
// Читаются здесь же, в parseRoute, а не отдельным хуком в компоненте: URL уже
|
||||
// является единственным источником правды для ownTab/ownStatus (см.
|
||||
// buildQuery — он не пишет значения по умолчанию в адрес), поэтому именно тут,
|
||||
// где URL молчит, естественно спросить «а что было в прошлый раз».
|
||||
// Пишутся ТОЛЬКО через persistOwnSelection — то есть только там, где
|
||||
// пользователь СОЗНАТЕЛЬНО меняет вкладку/фильтр (см. gotoOwnTab/setOwnStatus
|
||||
// в RequestPortal.tsx), а не при каждой навигации: иначе, например, просто
|
||||
// открыв и закрыв заявку, можно было бы затереть запомненное значение
|
||||
// значением по умолчанию.
|
||||
const OWN_TAB_STORAGE_KEY = 'rp-own-tab';
|
||||
const OWN_STATUS_STORAGE_KEY = 'rp-own-status';
|
||||
|
||||
function readStoredOwnTab(): OwnTab {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(OWN_TAB_STORAGE_KEY);
|
||||
return raw === 'approved' || raw === 'rejected' || raw === 'executed' ? raw : 'created';
|
||||
} catch {
|
||||
return 'created';
|
||||
}
|
||||
}
|
||||
|
||||
function readStoredOwnStatus(): string | undefined {
|
||||
try {
|
||||
return window.localStorage.getItem(OWN_STATUS_STORAGE_KEY) || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Запомнить подвкладку/фильтр «Моих заявок» — вызывать только при явном выборе пользователя. */
|
||||
export function persistOwnSelection(ownTab: OwnTab, ownStatus: string | undefined): void {
|
||||
try {
|
||||
window.localStorage.setItem(OWN_TAB_STORAGE_KEY, ownTab);
|
||||
if (ownStatus) {
|
||||
window.localStorage.setItem(OWN_STATUS_STORAGE_KEY, ownStatus);
|
||||
} else {
|
||||
window.localStorage.removeItem(OWN_STATUS_STORAGE_KEY);
|
||||
}
|
||||
} catch {
|
||||
// localStorage недоступен (приватный режим и т.п.) — просто не сохраняем.
|
||||
}
|
||||
}
|
||||
|
||||
function parseNav(raw: string | undefined): NavSection {
|
||||
if (
|
||||
raw === 'approvals' ||
|
||||
@@ -76,8 +124,8 @@ function parseNav(raw: string | undefined): NavSection {
|
||||
}
|
||||
|
||||
function parseOwnTab(raw: string | undefined): OwnTab {
|
||||
if (raw === 'approved' || raw === 'rejected' || raw === 'executed') return raw;
|
||||
return 'created';
|
||||
if (raw === 'approved' || raw === 'rejected' || raw === 'executed' || raw === 'created') return raw;
|
||||
return readStoredOwnTab();
|
||||
}
|
||||
|
||||
/** Разобрать строку запроса (по умолчанию — из адреса страницы) в маршрут. */
|
||||
@@ -96,7 +144,7 @@ export function parseRoute(search: string = window.location.search): IRoute {
|
||||
// Фильтр по статусу живёт только на вкладке «Инициированные»: у вкладок с
|
||||
// решениями отбор задаёт сама вкладка.
|
||||
const ownTab = nav === 'own' ? parseOwnTab(params.get('rptab') ?? undefined) : undefined;
|
||||
const ownStatus = ownTab === 'created' ? params.get('rpstatus') ?? undefined : undefined;
|
||||
const ownStatus = ownTab === 'created' ? params.get('rpstatus') ?? readStoredOwnStatus() : undefined;
|
||||
|
||||
// Открытая заявка (панель поверх раздела) — при полном наборе параметров.
|
||||
// Раздел под панелью сохраняется целиком, вместе с подвкладкой и фильтром:
|
||||
|
||||
Reference in New Issue
Block a user