Sync issues routing and shell URLs
- Move issue detail selection into route search so the modal is deep-linkable and back-button friendly. - Normalize issue category and detail params before they reach the loader. - Keep the legacy shell URL in sync for React-owned home pages. - Preserve the legacy issues-tour hooks on the React issues page. - Add Escape handling, focus trapping, and focus restore to the issue detail modal. - Add route and helper coverage for the new search-state behavior.
This commit is contained in:
parent
a2495aaba7
commit
fac4e1ba1a
8 changed files with 268 additions and 50 deletions
35
webui/src/routes/issues/-issues.helpers.test.ts
Normal file
35
webui/src/routes/issues/-issues.helpers.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, expect, it } from 'vite-plus/test';
|
||||
|
||||
import { normalizeIssuesSearch } from './-issues.helpers';
|
||||
|
||||
describe('normalizeIssuesSearch', () => {
|
||||
it('falls back to all for unknown categories', () => {
|
||||
expect(normalizeIssuesSearch({ category: 'not_real' })).toEqual({
|
||||
status: 'open',
|
||||
category: 'all',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves known categories', () => {
|
||||
expect(normalizeIssuesSearch({ category: 'wrong_metadata' })).toEqual({
|
||||
status: 'open',
|
||||
category: 'wrong_metadata',
|
||||
});
|
||||
});
|
||||
|
||||
it('drops invalid issue ids', () => {
|
||||
expect(normalizeIssuesSearch({ issueId: 'abc123' })).toEqual({
|
||||
status: 'open',
|
||||
category: 'all',
|
||||
issueId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes numeric issue ids', () => {
|
||||
expect(normalizeIssuesSearch({ issueId: '7' })).toEqual({
|
||||
status: 'open',
|
||||
category: 'all',
|
||||
issueId: 7,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -89,6 +89,12 @@ export const ISSUE_CATEGORY_META: Record<
|
|||
},
|
||||
};
|
||||
|
||||
const ISSUE_CATEGORY_KEYS = new Set(Object.keys(ISSUE_CATEGORY_META));
|
||||
|
||||
export type NormalizedIssuesSearch = Required<Pick<IssuesSearch, 'status' | 'category'>> & {
|
||||
issueId?: number;
|
||||
};
|
||||
|
||||
export const ISSUE_STATUS_META: Record<string, { label: string; className: string }> = {
|
||||
open: { label: 'Open', className: 'is-open' },
|
||||
in_progress: { label: 'In Progress', className: 'is-progress' },
|
||||
|
|
@ -113,9 +119,10 @@ export function createDefaultIssueTitle(category: string, entityName: string): s
|
|||
return `${label}: ${entityName || 'Unknown'}`;
|
||||
}
|
||||
|
||||
export function normalizeIssuesSearch(search: IssuesSearch | undefined): Required<IssuesSearch> {
|
||||
export function normalizeIssuesSearch(search: IssuesSearch | undefined): NormalizedIssuesSearch {
|
||||
const status = search?.status;
|
||||
const category = search?.category;
|
||||
const issueId = search?.issueId;
|
||||
|
||||
return {
|
||||
status:
|
||||
|
|
@ -126,7 +133,15 @@ export function normalizeIssuesSearch(search: IssuesSearch | undefined): Require
|
|||
status === 'dismissed'
|
||||
? status
|
||||
: DEFAULT_ISSUES_SEARCH.status,
|
||||
category: typeof category === 'string' && category.length > 0 ? category : 'all',
|
||||
category: typeof category === 'string' && ISSUE_CATEGORY_KEYS.has(category)
|
||||
? category
|
||||
: 'all',
|
||||
issueId:
|
||||
(typeof issueId === 'number' && Number.isInteger(issueId) && issueId > 0)
|
||||
? issueId
|
||||
: (typeof issueId === 'string' && /^[1-9]\d*$/.test(issueId)
|
||||
? Number(issueId)
|
||||
: undefined),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +159,7 @@ export async function fetchIssueCounts(profileId: number): Promise<IssueCounts>
|
|||
|
||||
export async function fetchIssueList(
|
||||
profileId: number,
|
||||
search: Required<IssuesSearch>,
|
||||
search: Pick<NormalizedIssuesSearch, 'status' | 'category'>,
|
||||
): Promise<IssueListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('limit', String(DEFAULT_LIMIT));
|
||||
|
|
@ -240,7 +255,10 @@ export function issueCountsQueryOptions(profileId: number) {
|
|||
});
|
||||
}
|
||||
|
||||
export function issueListQueryOptions(profileId: number, search: Required<IssuesSearch>) {
|
||||
export function issueListQueryOptions(
|
||||
profileId: number,
|
||||
search: Pick<NormalizedIssuesSearch, 'status' | 'category'>,
|
||||
) {
|
||||
return queryOptions({
|
||||
queryKey: ['issues', 'list', profileId, search.status, search.category],
|
||||
queryFn: () => fetchIssueList(profileId, search),
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export interface IssueCountsResponse {
|
|||
|
||||
export interface IssuesSearch {
|
||||
category?: string;
|
||||
issueId?: string | number;
|
||||
status?: IssueStatus | 'all';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,12 +28,16 @@ function createShellBridge(overrides: Partial<ShellBridge> = {}): ShellBridge {
|
|||
};
|
||||
}
|
||||
|
||||
function renderIssuesRoute() {
|
||||
function renderIssuesRoute(initialEntries = ['/issues']) {
|
||||
const queryClient = createAppQueryClient();
|
||||
const history = createMemoryHistory({ initialEntries: ['/issues'] });
|
||||
const history = createMemoryHistory({ initialEntries });
|
||||
const router = createAppRouter({ history, queryClient });
|
||||
|
||||
return render(<AppRouterProvider router={router} queryClient={queryClient} />);
|
||||
return {
|
||||
history,
|
||||
router,
|
||||
...render(<AppRouterProvider router={router} queryClient={queryClient} />),
|
||||
};
|
||||
}
|
||||
|
||||
const workflowActions = {
|
||||
|
|
@ -136,6 +140,11 @@ describe('issues route', () => {
|
|||
expect(await screen.findByTestId('issue-card-7')).toHaveTextContent('Bad tags');
|
||||
});
|
||||
|
||||
it('loads the detail modal from the route search state', async () => {
|
||||
renderIssuesRoute(['/issues?issueId=7']);
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toHaveTextContent('Issue #7'));
|
||||
});
|
||||
|
||||
it('stores filters in route search state', async () => {
|
||||
renderIssuesRoute();
|
||||
const status = await screen.findByRole('combobox', { name: /status/i });
|
||||
|
|
@ -144,11 +153,40 @@ describe('issues route', () => {
|
|||
});
|
||||
|
||||
it('opens and closes the detail modal', async () => {
|
||||
renderIssuesRoute();
|
||||
const { history } = renderIssuesRoute();
|
||||
fireEvent.click(await screen.findByTestId('issue-card-7'));
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toHaveTextContent('Issue #7'));
|
||||
await waitFor(() => expect(history.location.search).toContain('issueId=7'));
|
||||
fireEvent.click(screen.getByRole('button', { name: /close issue detail/i }));
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
|
||||
await waitFor(() => expect(history.location.search).toBe('?status=open&category=all'));
|
||||
});
|
||||
|
||||
it('closes the detail modal with Escape', async () => {
|
||||
const { history } = renderIssuesRoute();
|
||||
fireEvent.click(await screen.findByTestId('issue-card-7'));
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toHaveTextContent('Issue #7'));
|
||||
await waitFor(() => expect(history.location.search).toContain('issueId=7'));
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
|
||||
await waitFor(() => expect(history.location.search).toBe('?status=open&category=all'));
|
||||
});
|
||||
|
||||
it('traps focus inside the detail modal', async () => {
|
||||
renderIssuesRoute();
|
||||
fireEvent.click(await screen.findByTestId('issue-card-7'));
|
||||
|
||||
const closeButton = await screen.findByRole('button', { name: /close issue detail/i });
|
||||
const deleteButton = await screen.findByRole('button', { name: /delete/i });
|
||||
|
||||
deleteButton.focus();
|
||||
expect(deleteButton).toHaveFocus();
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Tab' });
|
||||
|
||||
expect(closeButton).toHaveFocus();
|
||||
});
|
||||
|
||||
it('invokes the shared workflow adapter for admin downloads', async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Button, FormField, TextArea } from '@/components/form';
|
||||
import {
|
||||
|
|
@ -40,11 +40,88 @@ export function IssueDetailModal({
|
|||
profileId: number;
|
||||
}) {
|
||||
const [adminResponse, setAdminResponse] = useState('');
|
||||
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||
const previouslyFocusedElementRef = useRef<HTMLElement | null>(null);
|
||||
const isOpen = Boolean(issue || isLoading || error);
|
||||
|
||||
useEffect(() => {
|
||||
setAdminResponse(issue?.admin_response || '');
|
||||
}, [issue?.admin_response, issue?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
previouslyFocusedElementRef.current = document.activeElement instanceof HTMLElement
|
||||
? document.activeElement
|
||||
: null;
|
||||
|
||||
const focusModal = () => {
|
||||
const modal = modalRef.current;
|
||||
if (!modal) return;
|
||||
|
||||
const focusable = getFocusableElements(modal);
|
||||
(focusable[0] || modal).focus();
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key !== 'Tab') return;
|
||||
|
||||
const modal = modalRef.current;
|
||||
if (!modal) return;
|
||||
|
||||
const focusable = getFocusableElements(modal);
|
||||
if (focusable.length === 0) {
|
||||
event.preventDefault();
|
||||
modal.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (activeElement === first || !modal.contains(activeElement)) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
}
|
||||
} else if (activeElement === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const onFocusIn = (event: FocusEvent) => {
|
||||
const modal = modalRef.current;
|
||||
if (!modal) return;
|
||||
if (event.target instanceof Node && !modal.contains(event.target)) {
|
||||
const focusable = getFocusableElements(modal);
|
||||
(focusable[0] || modal).focus();
|
||||
}
|
||||
};
|
||||
|
||||
const raf = requestAnimationFrame(focusModal);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener('focusin', onFocusIn);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener('focusin', onFocusIn);
|
||||
previouslyFocusedElementRef.current?.focus?.();
|
||||
previouslyFocusedElementRef.current = null;
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async (payload: { issueId: number; status: string; adminResponse: string }) => {
|
||||
await updateIssue(profileId, payload.issueId, {
|
||||
|
|
@ -197,6 +274,8 @@ export function IssueDetailModal({
|
|||
>
|
||||
<div
|
||||
className={`${styles.modal} ${styles.issueDetailModal}`}
|
||||
ref={modalRef}
|
||||
tabIndex={-1}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className={styles.modalHeader}>
|
||||
|
|
@ -524,6 +603,21 @@ export function IssueDetailModal({
|
|||
);
|
||||
}
|
||||
|
||||
function getFocusableElements(container: HTMLElement) {
|
||||
return Array.from(
|
||||
container.querySelectorAll<HTMLElement>(
|
||||
[
|
||||
'button:not([disabled])',
|
||||
'[href]',
|
||||
'input:not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
'[tabindex]:not([tabindex="-1"])',
|
||||
].join(','),
|
||||
),
|
||||
).filter((element) => element.tabIndex >= 0);
|
||||
}
|
||||
|
||||
function getStatusClassName(status: string) {
|
||||
if (status === 'in_progress') return styles.issueStatusProgress;
|
||||
if (status === 'resolved') return styles.issueStatusResolved;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Select } from '@/components/form';
|
||||
import { getShellProfileContext } from '@/platform/shell/bridge';
|
||||
|
|
@ -30,24 +30,39 @@ import { Route } from '../route';
|
|||
import { IssueDetailModal } from './issue-detail-modal';
|
||||
import styles from './issues-page.module.css';
|
||||
|
||||
type NavigateFunction = ReturnType<typeof useNavigate>;
|
||||
|
||||
function clearIssueSelection(navigate: NavigateFunction) {
|
||||
void navigate({
|
||||
search: (prev) => normalizeIssuesSearch({ ...prev, issueId: undefined }),
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function IssuesPage() {
|
||||
const bridge = useReactPageShell('issues');
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate({ from: Route.fullPath });
|
||||
const search = Route.useSearch();
|
||||
const normalizedSearch = normalizeIssuesSearch(search);
|
||||
const [selectedIssueId, setSelectedIssueId] = useState<number | null>(null);
|
||||
const selectedIssueId = normalizedSearch.issueId ? Number(normalizedSearch.issueId) : null;
|
||||
|
||||
const profile = getShellProfileContext(bridge);
|
||||
const profileId = profile?.profileId ?? 0;
|
||||
|
||||
const openIssue = (issueId: number) => {
|
||||
void navigate({
|
||||
search: (prev) => normalizeIssuesSearch({ ...prev, issueId }),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleRefresh = () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['issues'] });
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSelectedIssueId(null);
|
||||
clearIssueSelection(navigate);
|
||||
};
|
||||
|
||||
window.addEventListener(REFRESH_EVENT, handleRefresh);
|
||||
|
|
@ -56,7 +71,7 @@ export function IssuesPage() {
|
|||
window.removeEventListener(REFRESH_EVENT, handleRefresh);
|
||||
window.removeEventListener(CLOSE_EVENT, handleClose);
|
||||
};
|
||||
}, [queryClient]);
|
||||
}, [navigate, queryClient]);
|
||||
|
||||
const countsQuery = useQuery({
|
||||
...issueCountsQueryOptions(profileId),
|
||||
|
|
@ -86,14 +101,22 @@ export function IssuesPage() {
|
|||
issuesLoading={issuesQuery.isLoading}
|
||||
onCategoryChange={(category) =>
|
||||
void navigate({
|
||||
search: (prev) => normalizeIssuesSearch({ ...prev, category }),
|
||||
search: (prev) =>
|
||||
normalizeIssuesSearch({
|
||||
...prev,
|
||||
category,
|
||||
}),
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
onIssueSelect={setSelectedIssueId}
|
||||
onIssueSelect={openIssue}
|
||||
onStatusChange={(status) =>
|
||||
void navigate({
|
||||
search: (prev) => normalizeIssuesSearch({ ...prev, status }),
|
||||
search: (prev) =>
|
||||
normalizeIssuesSearch({
|
||||
...prev,
|
||||
status,
|
||||
}),
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
|
|
@ -104,9 +127,9 @@ export function IssuesPage() {
|
|||
issue={selectedIssueQuery.data ?? null}
|
||||
isLoading={selectedIssueQuery.isLoading}
|
||||
error={selectedIssueQuery.error}
|
||||
onClose={() => setSelectedIssueId(null)}
|
||||
onClose={() => clearIssueSelection(navigate)}
|
||||
onMutationSuccess={() => {
|
||||
setSelectedIssueId(null);
|
||||
clearIssueSelection(navigate);
|
||||
dispatchIssuesRefreshEvent();
|
||||
}}
|
||||
profileId={profile.profileId}
|
||||
|
|
@ -148,7 +171,7 @@ function IssueBoard({
|
|||
|
||||
return (
|
||||
<div className={styles.issuesContainer} data-testid="issues-board">
|
||||
<div className={styles.issuesHeader}>
|
||||
<div className={styles.issuesHeader} id="issues-header">
|
||||
<div className={styles.issuesHeaderLeft}>
|
||||
<h2 className={styles.issuesTitle}>Issues</h2>
|
||||
<p className={styles.issuesSubtitle} id="issues-subtitle">
|
||||
|
|
@ -201,7 +224,7 @@ function IssueBoard({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.issuesStats} data-testid="issue-counts">
|
||||
<div className={styles.issuesStats} id="issues-stats" data-testid="issue-counts">
|
||||
<div className={`${styles.issuesStatCard} ${styles.issuesStatOpen}`}>
|
||||
<div className={styles.issuesStatNumber}>{safeCounts.open}</div>
|
||||
<div className={styles.issuesStatLabel}>Open</div>
|
||||
|
|
@ -224,33 +247,33 @@ function IssueBoard({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{issuesLoading ? (
|
||||
<div className={styles.issuesLoading}>
|
||||
<div className={styles.issuesSpinner} />
|
||||
Loading issues...
|
||||
</div>
|
||||
) : issuesError ? (
|
||||
<div className={styles.issuesEmpty}>
|
||||
<div className={styles.issuesEmptyTitle}>Failed to load issues</div>
|
||||
<div className={styles.issuesEmptyText}>
|
||||
{issuesError instanceof Error ? issuesError.message : 'Unknown error'}
|
||||
<div className={styles.issuesList} id="issues-list" data-testid="issue-list">
|
||||
{issuesLoading ? (
|
||||
<div className={styles.issuesLoading}>
|
||||
<div className={styles.issuesSpinner} />
|
||||
Loading issues...
|
||||
</div>
|
||||
</div>
|
||||
) : issues.length === 0 ? (
|
||||
<div className={styles.issuesEmpty}>
|
||||
<div className={styles.issuesEmptyIcon} aria-hidden="true">
|
||||
🔍
|
||||
) : issuesError ? (
|
||||
<div className={styles.issuesEmpty}>
|
||||
<div className={styles.issuesEmptyTitle}>Failed to load issues</div>
|
||||
<div className={styles.issuesEmptyText}>
|
||||
{issuesError instanceof Error ? issuesError.message : 'Unknown error'}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.issuesEmptyTitle}>No issues found</div>
|
||||
<div className={styles.issuesEmptyText}>
|
||||
{statusFilter !== 'open' || categoryFilter !== 'all'
|
||||
? 'Try adjusting your filters'
|
||||
: 'No issues have been reported yet'}
|
||||
) : issues.length === 0 ? (
|
||||
<div className={styles.issuesEmpty}>
|
||||
<div className={styles.issuesEmptyIcon} aria-hidden="true">
|
||||
🔍
|
||||
</div>
|
||||
<div className={styles.issuesEmptyTitle}>No issues found</div>
|
||||
<div className={styles.issuesEmptyText}>
|
||||
{statusFilter !== 'open' || categoryFilter !== 'all'
|
||||
? 'Try adjusting your filters'
|
||||
: 'No issues have been reported yet'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.issuesList} data-testid="issue-list">
|
||||
{issues.map((issue) => {
|
||||
) : (
|
||||
issues.map((issue) => {
|
||||
const snapshot = parseSnapshot(issue.snapshot_data);
|
||||
const artwork = getIssueArtwork(snapshot);
|
||||
const entityName = getEntityName(issue, snapshot);
|
||||
|
|
@ -331,9 +354,9 @@ function IssueBoard({
|
|||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { getProfileHomePath, getShellProfileContext } from '@/platform/shell/bri
|
|||
|
||||
import {
|
||||
issueCountsQueryOptions,
|
||||
issueDetailQueryOptions,
|
||||
issueListQueryOptions,
|
||||
normalizeIssuesSearch,
|
||||
} from './-issues.helpers';
|
||||
|
|
@ -17,7 +18,11 @@ export const Route = createFileRoute('/issues')({
|
|||
throw redirect({ href: getProfileHomePath(bridge), replace: true });
|
||||
}
|
||||
},
|
||||
loaderDeps: ({ search }) => search,
|
||||
loaderDeps: ({ search }) => ({
|
||||
status: search.status,
|
||||
category: search.category,
|
||||
issueId: search.issueId ?? null,
|
||||
}),
|
||||
loader: async ({ context, deps }) => {
|
||||
const profile = getShellProfileContext(context.platform.getShellBridge());
|
||||
if (!profile) return;
|
||||
|
|
@ -25,6 +30,9 @@ export const Route = createFileRoute('/issues')({
|
|||
await Promise.all([
|
||||
context.queryClient.ensureQueryData(issueCountsQueryOptions(profile.profileId)),
|
||||
context.queryClient.ensureQueryData(issueListQueryOptions(profile.profileId, deps)),
|
||||
deps.issueId
|
||||
? context.queryClient.ensureQueryData(issueDetailQueryOptions(profile.profileId, deps.issueId))
|
||||
: Promise.resolve(),
|
||||
]);
|
||||
},
|
||||
component: IssuesPage,
|
||||
|
|
|
|||
|
|
@ -1178,14 +1178,15 @@ async function loadInitialData() {
|
|||
: homePage;
|
||||
|
||||
// Always apply the target page to the legacy shell chrome.
|
||||
// When the router is present, skipRouteChange keeps the URL stable
|
||||
// while still syncing the active page/nav state for direct loads.
|
||||
const router = getWebRouter();
|
||||
const route = router?.routeManifest?.find((entry) => entry.pageId === targetPage);
|
||||
|
||||
if (route?.kind === 'react') {
|
||||
showReactHost(targetPage);
|
||||
setActivePageChrome(targetPage);
|
||||
if (window.location.pathname !== route.path) {
|
||||
history.replaceState({ page: targetPage }, '', route.path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue