теперь можно пройти по заявку в мои заявки
This commit is contained in:
@@ -4,7 +4,6 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
margin-top: $sp-6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile {
|
.tile {
|
||||||
@@ -99,7 +98,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
margin-top: $sp-6;
|
|
||||||
color: $gray-500;
|
color: $gray-500;
|
||||||
font-size: $fs-sm;
|
font-size: $fs-sm;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,30 @@
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabsWrap {
|
||||||
|
margin-bottom: $sp-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возврат из режима согласования (approve) к списку заявок (SubmitFeature).
|
||||||
|
.backButton {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $sp-2;
|
||||||
|
margin-bottom: $sp-4;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: $navy-700;
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: $fs-sm;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color .18s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $gold-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// На узких экранах панель уходит под формы.
|
// На узких экранах панель уходит под формы.
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.layout {
|
.layout {
|
||||||
|
|||||||
@@ -2,42 +2,76 @@ import * as React from 'react';
|
|||||||
import styles from './RequestPortal.module.scss';
|
import styles from './RequestPortal.module.scss';
|
||||||
import type { IRequestPortalProps } from './IRequestPortalProps';
|
import type { IRequestPortalProps } from './IRequestPortalProps';
|
||||||
import { IconSprite } from '../shared/ui/Icon/IconSprite';
|
import { IconSprite } from '../shared/ui/Icon/IconSprite';
|
||||||
import { parseRoute } from '../shared/routing';
|
import { Icon } from '../shared/ui/Icon/Icon';
|
||||||
|
import { navigate, useRoute } from '../shared/routing';
|
||||||
|
import { CategoryTabs } from './CategoryTabs/CategoryTabs';
|
||||||
|
import { CATEGORIES, DEFAULT_CATEGORY_ID } from '../data/categories';
|
||||||
|
import { REQUEST_TYPES } from '../data/requests';
|
||||||
import SubmitFeature from '../features/submit/SubmitFeature';
|
import SubmitFeature from '../features/submit/SubmitFeature';
|
||||||
import ApproveFeature from '../features/approve/ApproveFeature';
|
import ApproveFeature from '../features/approve/ApproveFeature';
|
||||||
import MyRequestsPanel from '../features/myRequests/MyRequestsPanel';
|
import MyRequestsPanel from '../features/myRequests/MyRequestsPanel';
|
||||||
|
|
||||||
// Оболочка портала. Логики фич тут нет — файл общий, меняется редко.
|
// Оболочка портала. Логики фич тут нет — файл общий, меняется редко.
|
||||||
// Роутинг по URL: mode=approve&type=&id= → фича согласования, иначе — подача.
|
// Роутинг по URL: mode=approve&type=&id= → фича согласования, иначе — подача.
|
||||||
|
//
|
||||||
|
// Вкладки категорий рендерятся здесь (а не внутри SubmitFeature), чтобы
|
||||||
|
// оставаться на экране и в режиме согласования (approve) — раньше режим
|
||||||
|
// согласования подменял всю область main и вкладки пропадали вместе с ней.
|
||||||
const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context }) => {
|
const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context }) => {
|
||||||
const rootClasses = [styles.requestPortal, hasTeamsContext ? styles.teams : '']
|
const rootClasses = [styles.requestPortal, hasTeamsContext ? styles.teams : '']
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
const route = parseRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
const [activeCategoryId, setActiveCategoryId] = React.useState<string>(() => {
|
||||||
|
if (route.mode === 'create' && route.formKey) {
|
||||||
|
const found = REQUEST_TYPES.filter((r) => r.id === route.formKey)[0];
|
||||||
|
if (found) return found.categoryId;
|
||||||
|
}
|
||||||
|
return DEFAULT_CATEGORY_ID;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={rootClasses}>
|
<section className={rootClasses}>
|
||||||
<IconSprite />
|
<IconSprite />
|
||||||
{route.mode === 'approve' && route.formKey && route.itemId ? (
|
|
||||||
<ApproveFeature context={context} formKey={route.formKey} itemId={route.itemId} />
|
|
||||||
) : (
|
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
<div className={styles.side}>
|
<div className={styles.side}>
|
||||||
<MyRequestsPanel context={context} />
|
<MyRequestsPanel context={context} />
|
||||||
</div>
|
</div>
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
|
<div className={styles.tabsWrap}>
|
||||||
|
<CategoryTabs
|
||||||
|
categories={CATEGORIES}
|
||||||
|
activeId={activeCategoryId}
|
||||||
|
onChange={setActiveCategoryId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{route.mode === 'approve' && route.formKey && route.itemId ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.backButton}
|
||||||
|
onClick={() => navigate({ mode: 'submit' })}
|
||||||
|
>
|
||||||
|
<Icon name="arrow-left" size="sm" />
|
||||||
|
<span>К списку заявок</span>
|
||||||
|
</button>
|
||||||
|
<ApproveFeature context={context} formKey={route.formKey} itemId={route.itemId} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<SubmitFeature
|
<SubmitFeature
|
||||||
context={context}
|
context={context}
|
||||||
|
activeCategoryId={activeCategoryId}
|
||||||
initialForm={
|
initialForm={
|
||||||
route.mode === 'create' && route.formKey && route.sourceId
|
route.mode === 'create' && route.formKey && route.sourceId
|
||||||
? { formKey: route.formKey, sourceId: route.sourceId }
|
? { formKey: route.formKey, sourceId: route.sourceId }
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,6 +70,13 @@
|
|||||||
border: 1px solid $gray-200;
|
border: 1px solid $gray-200;
|
||||||
border-radius: $r-sm;
|
border-radius: $r-sm;
|
||||||
background: $gray-50;
|
background: $gray-50;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: .16s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $gold-400;
|
||||||
|
background: $gold-50;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemMain {
|
.itemMain {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
|||||||
import { getMyRequests } from './myRequests.service';
|
import { getMyRequests } from './myRequests.service';
|
||||||
import { IMyRequest } from './models/IMyRequest';
|
import { IMyRequest } from './models/IMyRequest';
|
||||||
import { REQUEST_STATUS } from '../../shared/approval';
|
import { REQUEST_STATUS } from '../../shared/approval';
|
||||||
|
import { navigate, routeUrl } from '../../shared/routing';
|
||||||
import styles from './MyRequestsPanel.module.scss';
|
import styles from './MyRequestsPanel.module.scss';
|
||||||
|
|
||||||
export interface IMyRequestsPanelProps {
|
export interface IMyRequestsPanelProps {
|
||||||
@@ -97,7 +98,21 @@ const MyRequestsPanel: React.FC<IMyRequestsPanelProps> = ({ context }) => {
|
|||||||
<div className={styles.groupTitle}>{group.requestTypeName}</div>
|
<div className={styles.groupTitle}>{group.requestTypeName}</div>
|
||||||
<ul className={styles.list}>
|
<ul className={styles.list}>
|
||||||
{group.rows.map((r) => (
|
{group.rows.map((r) => (
|
||||||
<li className={styles.item} key={`${r.requestTypeId}-${r.id}`}>
|
<li key={`${r.requestTypeId}-${r.id}`}>
|
||||||
|
<a
|
||||||
|
className={styles.item}
|
||||||
|
href={routeUrl({ mode: 'approve', formKey: r.formKey, itemId: r.id })}
|
||||||
|
onClick={(e) => {
|
||||||
|
// Обычный левый клик — SPA-переход (без перезагрузки страницы).
|
||||||
|
// Ctrl/Cmd/Shift/среднюю кнопку — не трогаем, пусть браузер сам
|
||||||
|
// откроет в новой вкладке по href.
|
||||||
|
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
navigate({ mode: 'approve', formKey: r.formKey, itemId: r.id });
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className={styles.itemMain}>
|
<div className={styles.itemMain}>
|
||||||
<span className={styles.itemTitle}>{r.title || `#${r.id}`}</span>
|
<span className={styles.itemTitle}>{r.title || `#${r.id}`}</span>
|
||||||
<span className={styles.itemAuthor}>
|
<span className={styles.itemAuthor}>
|
||||||
@@ -108,6 +123,7 @@ const MyRequestsPanel: React.FC<IMyRequestsPanelProps> = ({ context }) => {
|
|||||||
<span className={`${styles.status} ${statusClass(r.status)}`}>
|
<span className={`${styles.status} ${statusClass(r.status)}`}>
|
||||||
{r.status || '—'}
|
{r.status || '—'}
|
||||||
</span>
|
</span>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -11,4 +11,6 @@ export interface IMyRequest {
|
|||||||
requestTypeId: number;
|
requestTypeId: number;
|
||||||
requestTypeName: string;
|
requestTypeName: string;
|
||||||
section: string;
|
section: string;
|
||||||
|
/** Ключ формы (rptype в URL режима согласования) — поле pageUrl из approval-route API. */
|
||||||
|
formKey: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ async function getMyRequestsForType(
|
|||||||
requestTypeId: type.id,
|
requestTypeId: type.id,
|
||||||
requestTypeName: type.name,
|
requestTypeName: type.name,
|
||||||
section: type.section,
|
section: type.section,
|
||||||
|
formKey: type.pageUrl,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((r) => !!tokenEmail && r.authorEmail.toLowerCase() === tokenEmail);
|
.filter((r) => !!tokenEmail && r.authorEmail.toLowerCase() === tokenEmail);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
|
|
||||||
// ── Заглушка формы (до появления реальных форм) ──
|
// ── Заглушка формы (до появления реальных форм) ──
|
||||||
.formPlaceholder {
|
.formPlaceholder {
|
||||||
margin-top: $sp-6;
|
|
||||||
padding: $sp-8;
|
padding: $sp-8;
|
||||||
background: $white;
|
background: $white;
|
||||||
border: 1px solid $gray-200;
|
border: 1px solid $gray-200;
|
||||||
@@ -44,7 +43,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
margin-top: $sp-6;
|
|
||||||
padding: $sp-8;
|
padding: $sp-8;
|
||||||
color: $gray-500;
|
color: $gray-500;
|
||||||
font-size: $fs-sm;
|
font-size: $fs-sm;
|
||||||
|
|||||||
@@ -1,41 +1,39 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import styles from './SubmitFeature.module.scss';
|
import styles from './SubmitFeature.module.scss';
|
||||||
import { Icon } from '../../shared/ui/Icon/Icon';
|
import { Icon } from '../../shared/ui/Icon/Icon';
|
||||||
import { CategoryTabs } from '../../components/CategoryTabs/CategoryTabs';
|
|
||||||
import { RequestGrid } from '../../components/RequestGrid/RequestGrid';
|
import { RequestGrid } from '../../components/RequestGrid/RequestGrid';
|
||||||
import { CATEGORIES, DEFAULT_CATEGORY_ID } from '../../data/categories';
|
|
||||||
import { getRequestsByCategory, REQUEST_TYPES } from '../../data/requests';
|
import { getRequestsByCategory, REQUEST_TYPES } from '../../data/requests';
|
||||||
import { getFormComponent } from '../../forms/registry';
|
import { getFormComponent } from '../../forms/registry';
|
||||||
import { IRequestType } from '../../models/types';
|
import { IRequestType } from '../../models/types';
|
||||||
|
|
||||||
export interface ISubmitFeatureProps {
|
export interface ISubmitFeatureProps {
|
||||||
context: WebPartContext;
|
context: WebPartContext;
|
||||||
|
/** Активная категория. Вкладки теперь рендерятся на уровне портала (RequestPortal),
|
||||||
|
* чтобы оставаться на экране и в режиме согласования — эта фича их только слушает. */
|
||||||
|
activeCategoryId: string;
|
||||||
/** Открыть сразу форму с предзаполнением (производная заявка), минуя плитки. */
|
/** Открыть сразу форму с предзаполнением (производная заявка), минуя плитки. */
|
||||||
initialForm?: { formKey: string; sourceId: number };
|
initialForm?: { formKey: string; sourceId: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фича «Подать заявку»: категории → плитки → беспоук-форма.
|
// Фича «Подать заявку»: плитки → беспоук-форма (категории/вкладки — в RequestPortal).
|
||||||
// Владелец: автор портала. Живёт целиком в features/submit/**.
|
// Владелец: автор портала. Живёт целиком в features/submit/**.
|
||||||
const SubmitFeature: React.FC<ISubmitFeatureProps> = ({ context, initialForm }) => {
|
const SubmitFeature: React.FC<ISubmitFeatureProps> = ({ context, activeCategoryId, initialForm }) => {
|
||||||
// Если пришли по ссылке «создать на основе» — сразу открываем нужную форму.
|
// Если пришли по ссылке «создать на основе» — сразу открываем нужную форму.
|
||||||
const initialSelected = useMemo(
|
const initialSelected = useMemo(
|
||||||
() => (initialForm ? REQUEST_TYPES.filter((r) => r.id === initialForm.formKey)[0] ?? null : null),
|
() => (initialForm ? REQUEST_TYPES.filter((r) => r.id === initialForm.formKey)[0] ?? null : null),
|
||||||
[initialForm]
|
[initialForm]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [activeCategoryId, setActiveCategoryId] = useState<string>(
|
|
||||||
initialSelected ? initialSelected.categoryId : DEFAULT_CATEGORY_ID
|
|
||||||
);
|
|
||||||
const [selectedRequest, setSelectedRequest] = useState<IRequestType | null>(initialSelected);
|
const [selectedRequest, setSelectedRequest] = useState<IRequestType | null>(initialSelected);
|
||||||
|
|
||||||
const requests = getRequestsByCategory(activeCategoryId);
|
// Смена категории (вкладка сверху) — возвращаемся к списку плиток.
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedRequest((prev) => (prev && prev.categoryId !== activeCategoryId ? null : prev));
|
||||||
|
}, [activeCategoryId]);
|
||||||
|
|
||||||
const handleCategoryChange = (categoryId: string): void => {
|
const requests = getRequestsByCategory(activeCategoryId);
|
||||||
setActiveCategoryId(categoryId);
|
|
||||||
setSelectedRequest(null); // при смене категории возвращаемся к списку плиток
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = (): void => setSelectedRequest(null);
|
const handleBack = (): void => setSelectedRequest(null);
|
||||||
|
|
||||||
@@ -69,12 +67,6 @@ const SubmitFeature: React.FC<ISubmitFeatureProps> = ({ context, initialForm })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<CategoryTabs
|
|
||||||
categories={CATEGORIES}
|
|
||||||
activeId={activeCategoryId}
|
|
||||||
onChange={handleCategoryChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedRequest ? (
|
{selectedRequest ? (
|
||||||
renderSelected(selectedRequest)
|
renderSelected(selectedRequest)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
// Маршрутизация портала по параметрам URL.
|
// Маршрутизация портала по параметрам URL.
|
||||||
//
|
//
|
||||||
// Обычный режим (нет параметров) — подача заявок (features/submit).
|
// Обычный режим (нет параметров) — подача заявок (features/submit).
|
||||||
@@ -8,6 +10,11 @@
|
|||||||
// Префикс rp (request-portal): имена id/mode/type зарезервированы SharePoint
|
// Префикс rp (request-portal): имена id/mode/type зарезервированы SharePoint
|
||||||
// (напр. ?id= он трактует как навигацию к элементу → «Элемент не существует»),
|
// (напр. ?id= он трактует как навигацию к элементу → «Элемент не существует»),
|
||||||
// поэтому используем собственные, неконфликтные имена параметров.
|
// поэтому используем собственные, неконфликтные имена параметров.
|
||||||
|
//
|
||||||
|
// Навигация внутри портала (напр. клик по заявке в «Мои заявки») идёт через
|
||||||
|
// navigate() — это history.pushState без перезагрузки страницы. useRoute()
|
||||||
|
// подписан и на popstate (кнопка «назад»), и на её собственное событие,
|
||||||
|
// поэтому реагирует на оба способа смены URL.
|
||||||
|
|
||||||
export type PortalMode = 'submit' | 'approve' | 'create';
|
export type PortalMode = 'submit' | 'approve' | 'create';
|
||||||
|
|
||||||
@@ -21,6 +28,8 @@ export interface IRoute {
|
|||||||
sourceId?: number;
|
sourceId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROUTE_CHANGE_EVENT = 'rp-route-change';
|
||||||
|
|
||||||
/** Разобрать строку запроса (по умолчанию — из адреса страницы) в маршрут. */
|
/** Разобрать строку запроса (по умолчанию — из адреса страницы) в маршрут. */
|
||||||
export function parseRoute(search: string = window.location.search): IRoute {
|
export function parseRoute(search: string = window.location.search): IRoute {
|
||||||
const params = new URLSearchParams(search);
|
const params = new URLSearchParams(search);
|
||||||
@@ -41,3 +50,68 @@ export function parseRoute(search: string = window.location.search): IRoute {
|
|||||||
}
|
}
|
||||||
return { mode: 'submit' };
|
return { mode: 'submit' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перейти на другой маршрут без перезагрузки страницы (SPA): обновляет
|
||||||
|
* query-параметры адресной строки через history.pushState и оповещает
|
||||||
|
* useRoute() своим событием (popstate на pushState сам не срабатывает).
|
||||||
|
*/
|
||||||
|
export function navigate(route: IRoute): void {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
params.delete('rpmode');
|
||||||
|
params.delete('rptype');
|
||||||
|
params.delete('rpid');
|
||||||
|
params.delete('rpsource');
|
||||||
|
|
||||||
|
if (route.mode === 'approve' && route.formKey && route.itemId) {
|
||||||
|
params.set('rpmode', 'approve');
|
||||||
|
params.set('rptype', route.formKey);
|
||||||
|
params.set('rpid', String(route.itemId));
|
||||||
|
} else if (route.mode === 'create' && route.formKey && route.sourceId) {
|
||||||
|
params.set('rptype', route.formKey);
|
||||||
|
params.set('rpsource', String(route.sourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = params.toString();
|
||||||
|
const url = `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}`;
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
window.dispatchEvent(new Event(ROUTE_CHANGE_EVENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ссылка на маршрут (для href — открытие в новой вкладке, копирование и т.п.). */
|
||||||
|
export function routeUrl(route: IRoute): string {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
params.delete('rpmode');
|
||||||
|
params.delete('rptype');
|
||||||
|
params.delete('rpid');
|
||||||
|
params.delete('rpsource');
|
||||||
|
|
||||||
|
if (route.mode === 'approve' && route.formKey && route.itemId) {
|
||||||
|
params.set('rpmode', 'approve');
|
||||||
|
params.set('rptype', route.formKey);
|
||||||
|
params.set('rpid', String(route.itemId));
|
||||||
|
} else if (route.mode === 'create' && route.formKey && route.sourceId) {
|
||||||
|
params.set('rptype', route.formKey);
|
||||||
|
params.set('rpsource', String(route.sourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = params.toString();
|
||||||
|
return `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Текущий маршрут, реактивно обновляется по navigate() и кнопке «назад». */
|
||||||
|
export function useRoute(): IRoute {
|
||||||
|
const [route, setRoute] = React.useState<IRoute>(() => parseRoute());
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onChange = (): void => setRoute(parseRoute());
|
||||||
|
window.addEventListener('popstate', onChange);
|
||||||
|
window.addEventListener(ROUTE_CHANGE_EVENT, onChange);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('popstate', onChange);
|
||||||
|
window.removeEventListener(ROUTE_CHANGE_EVENT, onChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user