Promote shell context to the root route
- Wait for the legacy shell bridge/profile before React routes render - Expose the shell bridge and profile through root TanStack context - Update issue routes and shell helpers to consume the shared context - Remove the redundant issues search normalization on read - Refresh the affected tests around shell bootstrap and routing
This commit is contained in:
parent
b34cea3388
commit
39f56fe63f
11 changed files with 194 additions and 89 deletions
|
|
@ -7,7 +7,7 @@ import { ROUTER_ROOT_ID } from '@/platform/shell/route-controllers';
|
||||||
import { createAppQueryClient } from './query-client';
|
import { createAppQueryClient } from './query-client';
|
||||||
import { AppRouterProvider, createAppRouter } from './router';
|
import { AppRouterProvider, createAppRouter } from './router';
|
||||||
|
|
||||||
export function bootstrapApp() {
|
export async function bootstrapApp() {
|
||||||
const container = document.getElementById(ROUTER_ROOT_ID);
|
const container = document.getElementById(ROUTER_ROOT_ID);
|
||||||
if (!container) return null;
|
if (!container) return null;
|
||||||
|
|
||||||
|
|
@ -20,4 +20,4 @@ export function bootstrapApp() {
|
||||||
return { queryClient, router };
|
return { queryClient, router };
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapApp();
|
void bootstrapApp();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@ import { createMemoryHistory } from '@tanstack/react-router';
|
||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import type { ShellBridge, ShellPageId } from '@/platform/shell/bridge';
|
import {
|
||||||
|
SHELL_PROFILE_CONTEXT_CHANGED_EVENT,
|
||||||
|
type ShellBridge,
|
||||||
|
type ShellPageId,
|
||||||
|
} from '@/platform/shell/bridge';
|
||||||
|
|
||||||
import { createAppQueryClient } from './query-client';
|
import { createAppQueryClient } from './query-client';
|
||||||
import { AppRouterProvider, createAppRouter } from './router';
|
import { AppRouterProvider, createAppRouter } from './router';
|
||||||
|
|
@ -129,10 +133,30 @@ describe('createAppRouter', () => {
|
||||||
render(<AppRouterProvider router={router} queryClient={queryClient} />);
|
render(<AppRouterProvider router={router} queryClient={queryClient} />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(window.SoulSyncWebShellBridge?.activateLegacyPath).toHaveBeenCalledWith('/discover');
|
expect(history.location.pathname).toBe('/discover');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('waits for profile context before rendering React routes', async () => {
|
||||||
|
const getCurrentProfileContext = vi.fn(() => null);
|
||||||
|
window.SoulSyncWebShellBridge = createShellBridge({
|
||||||
|
getCurrentProfileContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(history.location.pathname).toBe('/discover');
|
const queryClient = createAppQueryClient();
|
||||||
|
const history = createMemoryHistory({ initialEntries: ['/issues'] });
|
||||||
|
const router = createAppRouter({ history, queryClient });
|
||||||
|
|
||||||
|
render(<AppRouterProvider router={router} queryClient={queryClient} />);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('issues-board')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
getCurrentProfileContext.mockReturnValue({ profileId: 1, isAdmin: false });
|
||||||
|
window.dispatchEvent(new CustomEvent(SHELL_PROFILE_CONTEXT_CHANGED_EVENT));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('issues-board')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('redirects the root route to the profile home page', async () => {
|
it('redirects the root route to the profile home page', async () => {
|
||||||
|
|
|
||||||
59
webui/src/platform/shell/bridge.test.ts
Normal file
59
webui/src/platform/shell/bridge.test.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type { ShellProfileContext } from './bridge';
|
||||||
|
|
||||||
|
import { SHELL_PROFILE_CONTEXT_CHANGED_EVENT, waitForShellContext } from './bridge';
|
||||||
|
|
||||||
|
describe('waitForShellContext', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.SoulSyncWebShellBridge = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves immediately when the shell already has a profile', async () => {
|
||||||
|
window.SoulSyncWebShellBridge = {
|
||||||
|
getProfileHomePage: vi.fn(() => 'discover'),
|
||||||
|
isPageAllowed: vi.fn(() => true),
|
||||||
|
activateLegacyPath: vi.fn(),
|
||||||
|
getCurrentPageId: vi.fn(() => 'issues'),
|
||||||
|
getCurrentProfileContext: vi.fn(() => ({ profileId: 2, isAdmin: true })),
|
||||||
|
resolveLegacyPath: vi.fn(() => 'issues'),
|
||||||
|
setActivePageChrome: vi.fn(),
|
||||||
|
showReactHost: vi.fn(),
|
||||||
|
} as NonNullable<typeof window.SoulSyncWebShellBridge>;
|
||||||
|
|
||||||
|
await expect(waitForShellContext()).resolves.toEqual({
|
||||||
|
bridge: window.SoulSyncWebShellBridge,
|
||||||
|
profile: {
|
||||||
|
profileId: 2,
|
||||||
|
isAdmin: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('waits for the legacy shell to publish profile context', async () => {
|
||||||
|
const getCurrentProfileContext = vi.fn<() => ShellProfileContext | null>(() => null);
|
||||||
|
window.SoulSyncWebShellBridge = {
|
||||||
|
getProfileHomePage: vi.fn(() => 'discover'),
|
||||||
|
isPageAllowed: vi.fn(() => true),
|
||||||
|
activateLegacyPath: vi.fn(),
|
||||||
|
getCurrentPageId: vi.fn(() => 'issues'),
|
||||||
|
getCurrentProfileContext,
|
||||||
|
resolveLegacyPath: vi.fn(() => 'issues'),
|
||||||
|
setActivePageChrome: vi.fn(),
|
||||||
|
showReactHost: vi.fn(),
|
||||||
|
} as NonNullable<typeof window.SoulSyncWebShellBridge>;
|
||||||
|
|
||||||
|
const contextPromise = waitForShellContext();
|
||||||
|
|
||||||
|
getCurrentProfileContext.mockReturnValue({ profileId: 5, isAdmin: false });
|
||||||
|
window.dispatchEvent(new CustomEvent(SHELL_PROFILE_CONTEXT_CHANGED_EVENT));
|
||||||
|
|
||||||
|
await expect(contextPromise).resolves.toEqual({
|
||||||
|
bridge: window.SoulSyncWebShellBridge,
|
||||||
|
profile: {
|
||||||
|
profileId: 5,
|
||||||
|
isAdmin: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -14,8 +14,16 @@ export interface ShellProfileContext {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShellContext {
|
||||||
|
bridge: ShellBridge;
|
||||||
|
profile: ShellProfileContext;
|
||||||
|
}
|
||||||
|
|
||||||
export type ShellBridge = NonNullable<typeof window.SoulSyncWebShellBridge>;
|
export type ShellBridge = NonNullable<typeof window.SoulSyncWebShellBridge>;
|
||||||
|
|
||||||
|
export const SHELL_BRIDGE_READY_EVENT = 'ss:webui-shell-bridge-ready';
|
||||||
|
export const SHELL_PROFILE_CONTEXT_CHANGED_EVENT = 'ss:webui-profile-context-changed';
|
||||||
|
|
||||||
export function getShellBridge(): ShellBridge | null {
|
export function getShellBridge(): ShellBridge | null {
|
||||||
return window.SoulSyncWebShellBridge ?? null;
|
return window.SoulSyncWebShellBridge ?? null;
|
||||||
}
|
}
|
||||||
|
|
@ -24,11 +32,50 @@ export function getShellProfileContext(bridge = getShellBridge()): ShellProfileC
|
||||||
return bridge?.getCurrentProfileContext() ?? null;
|
return bridge?.getCurrentProfileContext() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getShellContext(bridge = getShellBridge()): ShellContext | null {
|
||||||
|
const profile = getShellProfileContext(bridge);
|
||||||
|
if (!bridge || !profile) return null;
|
||||||
|
|
||||||
|
return { bridge, profile };
|
||||||
|
}
|
||||||
|
|
||||||
export function getProfileHomePath(bridge = getShellBridge()): `/${string}` {
|
export function getProfileHomePath(bridge = getShellBridge()): `/${string}` {
|
||||||
const pageId = bridge?.getProfileHomePage() ?? 'discover';
|
const pageId = bridge?.getProfileHomePage() ?? 'discover';
|
||||||
return getShellRouteByPageId(pageId)?.path ?? '/discover';
|
return getShellRouteByPageId(pageId)?.path ?? '/discover';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function waitForShellContext(): Promise<ShellContext> {
|
||||||
|
const currentContext = getShellContext();
|
||||||
|
if (currentContext) return currentContext;
|
||||||
|
|
||||||
|
return await new Promise<ShellContext>((resolve) => {
|
||||||
|
const cleanup = () => {
|
||||||
|
window.removeEventListener(SHELL_BRIDGE_READY_EVENT, handleReady);
|
||||||
|
window.removeEventListener(SHELL_PROFILE_CONTEXT_CHANGED_EVENT, handleProfileChange);
|
||||||
|
};
|
||||||
|
|
||||||
|
const settleIfReady = () => {
|
||||||
|
const shell = getShellContext();
|
||||||
|
if (!shell) return;
|
||||||
|
cleanup();
|
||||||
|
resolve(shell);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReady = () => {
|
||||||
|
settleIfReady();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileChange = () => {
|
||||||
|
settleIfReady();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener(SHELL_BRIDGE_READY_EVENT, handleReady);
|
||||||
|
window.addEventListener(SHELL_PROFILE_CONTEXT_CHANGED_EVENT, handleProfileChange);
|
||||||
|
|
||||||
|
settleIfReady();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function bindWindowWebRouter(router: AnyRouter) {
|
export function bindWindowWebRouter(router: AnyRouter) {
|
||||||
window.SoulSyncWebRouter = {
|
window.SoulSyncWebRouter = {
|
||||||
routeManifest: [...shellRouteManifest],
|
routeManifest: [...shellRouteManifest],
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,24 @@
|
||||||
import { useRouter } from '@tanstack/react-router';
|
import { useRouteContext, useRouter } from '@tanstack/react-router';
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useLayoutEffect } from 'react';
|
||||||
|
|
||||||
import { getProfileHomePath, getShellBridge, type ShellPageId } from './bridge';
|
import { getProfileHomePath, type ShellContext, type ShellPageId } from './bridge';
|
||||||
|
|
||||||
export const ROUTER_ROOT_ID = 'webui-react-root';
|
export const ROUTER_ROOT_ID = 'webui-react-root';
|
||||||
export const SHELL_BRIDGE_READY_EVENT = 'ss:webui-shell-bridge-ready';
|
|
||||||
export const SHELL_PROFILE_CONTEXT_CHANGED_EVENT = 'ss:webui-profile-context-changed';
|
export function useShellContext(): ShellContext {
|
||||||
|
const context = useRouteContext({
|
||||||
|
from: '__root__',
|
||||||
|
select: (routeContext) => routeContext.shell,
|
||||||
|
});
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
export function useShellBridge() {
|
export function useShellBridge() {
|
||||||
const [, setRevision] = useState(0);
|
return useShellContext().bridge;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
export function useProfile() {
|
||||||
const handleContextChange = () => {
|
return useShellContext().profile;
|
||||||
setRevision((value) => value + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleContextChange();
|
|
||||||
window.addEventListener(SHELL_BRIDGE_READY_EVENT, handleContextChange);
|
|
||||||
window.addEventListener(SHELL_PROFILE_CONTEXT_CHANGED_EVENT, handleContextChange);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener(SHELL_BRIDGE_READY_EVENT, handleContextChange);
|
|
||||||
window.removeEventListener(SHELL_PROFILE_CONTEXT_CHANGED_EVENT, handleContextChange);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return getShellBridge();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LegacyRouteController({ pathname }: { pathname: string }) {
|
export function LegacyRouteController({ pathname }: { pathname: string }) {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,15 @@ import { Outlet, createRootRouteWithContext } from '@tanstack/react-router';
|
||||||
|
|
||||||
import type { AppRouterContext } from '@/app/router';
|
import type { AppRouterContext } from '@/app/router';
|
||||||
|
|
||||||
|
import { waitForShellContext } from '@/platform/shell/bridge';
|
||||||
|
|
||||||
import { IssueDomainHost } from './issues/-ui/issue-domain-host';
|
import { IssueDomainHost } from './issues/-ui/issue-domain-host';
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<AppRouterContext>()({
|
export const Route = createRootRouteWithContext<AppRouterContext>()({
|
||||||
|
beforeLoad: async () => {
|
||||||
|
const shell = await waitForShellContext();
|
||||||
|
return { shell };
|
||||||
|
},
|
||||||
component: () => (
|
component: () => (
|
||||||
<>
|
<>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@ export const Route = createFileRoute('/')({
|
||||||
beforeLoad: ({ context, location }) => {
|
beforeLoad: ({ context, location }) => {
|
||||||
if (location.pathname !== '/') return;
|
if (location.pathname !== '/') return;
|
||||||
|
|
||||||
const bridge = context.platform.getShellBridge();
|
const { bridge } = context.shell;
|
||||||
if (!bridge) return;
|
|
||||||
|
|
||||||
throw redirect({ href: getProfileHomePath(bridge), replace: true });
|
throw redirect({ href: getProfileHomePath(bridge), replace: true });
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||||
|
|
||||||
import { DialogBody, DialogFooter, DialogFrame, DialogHeader } from '@/components/dialog';
|
import { DialogBody, DialogFooter, DialogFrame, DialogHeader } from '@/components/dialog';
|
||||||
import { Button } from '@/components/form';
|
import { Button } from '@/components/form';
|
||||||
|
import { useProfile } from '@/platform/shell/route-controllers';
|
||||||
import {
|
import {
|
||||||
launchAlbumDownloadWorkflow,
|
launchAlbumDownloadWorkflow,
|
||||||
launchAlbumWishlistWorkflow,
|
launchAlbumWishlistWorkflow,
|
||||||
|
|
@ -22,21 +23,18 @@ import {
|
||||||
import styles from './issue-detail-modal.module.css';
|
import styles from './issue-detail-modal.module.css';
|
||||||
|
|
||||||
export function IssueDetailModal({
|
export function IssueDetailModal({
|
||||||
isAdmin,
|
|
||||||
issueId,
|
issueId,
|
||||||
onClose,
|
onClose,
|
||||||
onMutationSuccess,
|
onMutationSuccess,
|
||||||
profileId,
|
|
||||||
}: {
|
}: {
|
||||||
isAdmin: boolean;
|
issueId?: number;
|
||||||
issueId: number | null;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onMutationSuccess: () => void;
|
onMutationSuccess: () => void;
|
||||||
profileId: number;
|
|
||||||
}) {
|
}) {
|
||||||
|
const { isAdmin, profileId } = useProfile();
|
||||||
const selectedIssueQuery = useQuery({
|
const selectedIssueQuery = useQuery({
|
||||||
...issueDetailQueryOptions(profileId, issueId ?? 0),
|
...issueDetailQueryOptions(profileId, issueId ?? 0),
|
||||||
enabled: profileId > 0 && issueId !== null,
|
enabled: issueId != null,
|
||||||
});
|
});
|
||||||
const issue = selectedIssueQuery.data ?? null;
|
const issue = selectedIssueQuery.data ?? null;
|
||||||
const queryError = selectedIssueQuery.error;
|
const queryError = selectedIssueQuery.error;
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@ import {
|
||||||
TextArea,
|
TextArea,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from '@/components/form';
|
} from '@/components/form';
|
||||||
import { getShellProfileContext } from '@/platform/shell/bridge';
|
import { useProfile } from '@/platform/shell/route-controllers';
|
||||||
import { useShellBridge } from '@/platform/shell/route-controllers';
|
|
||||||
|
|
||||||
import type { IssuePriority, IssueReportPayload } from '../-issues.types';
|
import type { IssuePriority, IssueReportPayload } from '../-issues.types';
|
||||||
|
|
||||||
|
|
@ -44,15 +43,13 @@ const DEFAULT_REPORT_ISSUE_VALUES: ReportIssueFormValues = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function IssueDomainHost() {
|
export function IssueDomainHost() {
|
||||||
const bridge = useShellBridge();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const profile = getShellProfileContext(bridge);
|
const profile = useProfile();
|
||||||
const [reportPayload, setReportPayload] = useState<IssueReportPayload | null>(null);
|
const [reportPayload, setReportPayload] = useState<IssueReportPayload | null>(null);
|
||||||
const profileId = profile?.profileId ?? 0;
|
const profileId = profile.profileId;
|
||||||
|
|
||||||
const countsQuery = useQuery({
|
const countsQuery = useQuery({
|
||||||
...issueCountsQueryOptions(profileId),
|
...issueCountsQueryOptions(profileId),
|
||||||
enabled: profileId > 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import { useNavigate } from '@tanstack/react-router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { Select } from '@/components/form';
|
import { Select } from '@/components/form';
|
||||||
import { getShellProfileContext } from '@/platform/shell/bridge';
|
import { useProfile, useReactPageShell } from '@/platform/shell/route-controllers';
|
||||||
import { useReactPageShell } from '@/platform/shell/route-controllers';
|
|
||||||
|
|
||||||
import type { IssueCounts, IssueRecord, IssueStatus } from '../-issues.types';
|
import type { IssueCounts, IssueRecord, IssueStatus } from '../-issues.types';
|
||||||
|
|
||||||
|
|
@ -28,75 +27,59 @@ import { Route } from '../route';
|
||||||
import { IssueDetailModal } from './issue-detail-modal';
|
import { IssueDetailModal } from './issue-detail-modal';
|
||||||
import styles from './issues-page.module.css';
|
import styles from './issues-page.module.css';
|
||||||
|
|
||||||
type NavigateFunction = ReturnType<typeof useNavigate>;
|
|
||||||
|
|
||||||
function clearIssueSelection(navigate: NavigateFunction) {
|
|
||||||
void navigate({
|
|
||||||
to: Route.fullPath,
|
|
||||||
search: (prev) => normalizeIssuesSearch({ ...prev, issueId: undefined }),
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IssuesPage() {
|
export function IssuesPage() {
|
||||||
const bridge = useReactPageShell('issues');
|
useReactPageShell('issues');
|
||||||
|
const { isAdmin, profileId } = useProfile();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate({ from: Route.fullPath });
|
const navigate = useNavigate({ from: Route.fullPath });
|
||||||
const search = Route.useSearch();
|
const params = Route.useSearch();
|
||||||
const normalizedSearch = normalizeIssuesSearch(search);
|
|
||||||
const selectedIssueId = normalizedSearch.issueId ? Number(normalizedSearch.issueId) : null;
|
|
||||||
|
|
||||||
const profile = getShellProfileContext(bridge);
|
|
||||||
const profileId = profile?.profileId ?? 0;
|
|
||||||
|
|
||||||
const openIssue = (issueId: number) => {
|
const openIssue = (issueId: number) => {
|
||||||
void navigate({
|
navigate({
|
||||||
to: Route.fullPath,
|
to: Route.fullPath,
|
||||||
search: (prev) => normalizeIssuesSearch({ ...prev, issueId }),
|
search: (prev) => normalizeIssuesSearch({ ...prev, issueId }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearIssueSelection = () => {
|
||||||
|
navigate({
|
||||||
|
to: Route.fullPath,
|
||||||
|
search: (prev) => normalizeIssuesSearch({ ...prev, issueId: undefined }),
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ['issues'] });
|
queryClient.invalidateQueries({ queryKey: ['issues'] });
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
clearIssueSelection(navigate);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener(REFRESH_EVENT, handleRefresh);
|
window.addEventListener(REFRESH_EVENT, handleRefresh);
|
||||||
window.addEventListener(CLOSE_EVENT, handleClose);
|
window.addEventListener(CLOSE_EVENT, clearIssueSelection);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener(REFRESH_EVENT, handleRefresh);
|
window.removeEventListener(REFRESH_EVENT, handleRefresh);
|
||||||
window.removeEventListener(CLOSE_EVENT, handleClose);
|
window.removeEventListener(CLOSE_EVENT, clearIssueSelection);
|
||||||
};
|
};
|
||||||
}, [navigate, queryClient]);
|
}, [navigate, queryClient]);
|
||||||
|
|
||||||
const countsQuery = useQuery({
|
const countsQuery = useQuery({
|
||||||
...issueCountsQueryOptions(profileId),
|
...issueCountsQueryOptions(profileId),
|
||||||
enabled: profileId > 0,
|
|
||||||
});
|
});
|
||||||
const issuesQuery = useQuery({
|
const issuesQuery = useQuery({
|
||||||
...issueListQueryOptions(profileId, normalizedSearch),
|
...issueListQueryOptions(profileId, params),
|
||||||
enabled: profileId > 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!bridge || !profile || !bridge.isPageAllowed('issues')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IssueBoard
|
<IssueBoard
|
||||||
categoryFilter={normalizedSearch.category}
|
categoryFilter={params.category}
|
||||||
counts={countsQuery.data}
|
counts={countsQuery.data}
|
||||||
isAdmin={profile.isAdmin}
|
isAdmin={isAdmin}
|
||||||
issues={issuesQuery.data?.issues ?? []}
|
issues={issuesQuery.data?.issues ?? []}
|
||||||
issuesError={issuesQuery.error}
|
issuesError={issuesQuery.error}
|
||||||
issuesLoading={issuesQuery.isLoading}
|
issuesLoading={issuesQuery.isLoading}
|
||||||
onCategoryChange={(category) =>
|
onCategoryChange={(category) =>
|
||||||
void navigate({
|
navigate({
|
||||||
to: Route.fullPath,
|
to: Route.fullPath,
|
||||||
search: (prev) =>
|
search: (prev) =>
|
||||||
normalizeIssuesSearch({
|
normalizeIssuesSearch({
|
||||||
|
|
@ -108,7 +91,7 @@ export function IssuesPage() {
|
||||||
}
|
}
|
||||||
onIssueSelect={openIssue}
|
onIssueSelect={openIssue}
|
||||||
onStatusChange={(status) =>
|
onStatusChange={(status) =>
|
||||||
void navigate({
|
navigate({
|
||||||
to: Route.fullPath,
|
to: Route.fullPath,
|
||||||
search: (prev) =>
|
search: (prev) =>
|
||||||
normalizeIssuesSearch({
|
normalizeIssuesSearch({
|
||||||
|
|
@ -118,17 +101,15 @@ export function IssuesPage() {
|
||||||
replace: true,
|
replace: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
statusFilter={normalizedSearch.status}
|
statusFilter={params.status}
|
||||||
/>
|
/>
|
||||||
<IssueDetailModal
|
<IssueDetailModal
|
||||||
isAdmin={profile.isAdmin}
|
issueId={params.issueId}
|
||||||
issueId={selectedIssueId}
|
onClose={clearIssueSelection}
|
||||||
onClose={() => clearIssueSelection(navigate)}
|
|
||||||
onMutationSuccess={() => {
|
onMutationSuccess={() => {
|
||||||
clearIssueSelection(navigate);
|
clearIssueSelection();
|
||||||
dispatchIssuesRefreshEvent();
|
dispatchIssuesRefreshEvent();
|
||||||
}}
|
}}
|
||||||
profileId={profile.profileId}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||||
|
|
||||||
import { getProfileHomePath, getShellProfileContext } from '@/platform/shell/bridge';
|
import { getProfileHomePath } from '@/platform/shell/bridge';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
issueCountsQueryOptions,
|
issueCountsQueryOptions,
|
||||||
|
|
@ -13,8 +13,9 @@ import { IssuesPage } from './-ui/issues-page';
|
||||||
export const Route = createFileRoute('/issues')({
|
export const Route = createFileRoute('/issues')({
|
||||||
validateSearch: normalizeIssuesSearch,
|
validateSearch: normalizeIssuesSearch,
|
||||||
beforeLoad: ({ context }) => {
|
beforeLoad: ({ context }) => {
|
||||||
const bridge = context.platform.getShellBridge();
|
const { bridge } = context.shell;
|
||||||
if (bridge && !bridge.isPageAllowed('issues')) {
|
|
||||||
|
if (!bridge.isPageAllowed('issues')) {
|
||||||
throw redirect({ href: getProfileHomePath(bridge), replace: true });
|
throw redirect({ href: getProfileHomePath(bridge), replace: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -24,8 +25,7 @@ export const Route = createFileRoute('/issues')({
|
||||||
issueId: search.issueId ?? null,
|
issueId: search.issueId ?? null,
|
||||||
}),
|
}),
|
||||||
loader: async ({ context, deps }) => {
|
loader: async ({ context, deps }) => {
|
||||||
const profile = getShellProfileContext(context.platform.getShellBridge());
|
const { profile } = context.shell;
|
||||||
if (!profile) return;
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
context.queryClient.ensureQueryData(issueCountsQueryOptions(profile.profileId)),
|
context.queryClient.ensureQueryData(issueCountsQueryOptions(profile.profileId)),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue