From c90b42c9c4e82048d2a3145b8942fe2275bd68bc Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 18 Jul 2026 16:15:52 +0700 Subject: [PATCH] =?UTF-8?q?fix(web):=20=D1=80=D0=B5=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D0=B2=D0=BD=D0=BE=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D1=81=D1=82=D0=B2=D0=BE=20=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20SpaceProvider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit localStorage не вызывал re-render; useSpaceId + remount main по spaceId. Co-authored-by: Cursor --- apps/web/src/components/layout/app-shell.tsx | 15 +++++- .../src/components/layout/space-switcher.tsx | 16 ++++--- apps/web/src/components/vps-access-sheet.tsx | 5 +- apps/web/src/lib/space.ts | 47 +++++++++++++++++++ apps/web/src/queries/snapshot.ts | 14 ++++-- apps/web/src/routes/_auth/spaces.tsx | 4 +- 6 files changed, 84 insertions(+), 17 deletions(-) diff --git a/apps/web/src/components/layout/app-shell.tsx b/apps/web/src/components/layout/app-shell.tsx index 126cdc7..ceb0616 100644 --- a/apps/web/src/components/layout/app-shell.tsx +++ b/apps/web/src/components/layout/app-shell.tsx @@ -57,6 +57,7 @@ import { can, permissionForPath, } from '@/lib/auth' +import { SpaceProvider, useSpaceId } from '@/lib/space' interface NavItem { to: string @@ -162,6 +163,7 @@ export function AppShell({ children }: { children: ReactNode }) { })).filter((g) => g.items.length > 0) return ( +
- {children} + {children}
+
+ ) +} + +/** Remount route content when space changes so snapshot queries pick new X-Space-Id. */ +function SpaceScopedMain({ children }: { children: ReactNode }) { + const { spaceId } = useSpaceId() + return ( +
+ {children} +
) } diff --git a/apps/web/src/components/layout/space-switcher.tsx b/apps/web/src/components/layout/space-switcher.tsx index c872b6f..c216782 100644 --- a/apps/web/src/components/layout/space-switcher.tsx +++ b/apps/web/src/components/layout/space-switcher.tsx @@ -30,26 +30,28 @@ import { } from '@cfdm/ui/components/sidebar' import { api } from '@/lib/api-client' -import { getStoredSpaceId, setStoredSpaceId, type SpaceDto } from '@/lib/space' +import { useSpaceId, type SpaceDto } from '@/lib/space' import { spacesKeys, spacesQueryOptions, snapshotKeys } from '@/queries/snapshot' export function SpaceSwitcher() { const { isMobile } = useSidebar() const qc = useQueryClient() + const { spaceId, setSpaceId } = useSpaceId() const { data: spaces = [] } = useQuery(spacesQueryOptions()) - const currentId = getStoredSpaceId() ?? spaces[0]?.id + const currentId = spaceId ?? spaces[0]?.id const current = spaces.find((s) => s.id === currentId) ?? spaces[0] const [createOpen, setCreateOpen] = useState(false) const [name, setName] = useState('') useEffect(() => { - if (!getStoredSpaceId() && spaces[0]?.id) { - setStoredSpaceId(spaces[0].id) + if (!spaceId && spaces[0]?.id) { + setSpaceId(spaces[0].id) } - }, [spaces]) + }, [spaceId, spaces, setSpaceId]) function selectSpace(space: SpaceDto) { - setStoredSpaceId(space.id) + if (space.id === currentId) return + setSpaceId(space.id) void qc.invalidateQueries({ queryKey: snapshotKeys.all }) void qc.invalidateQueries({ queryKey: spacesKeys.all }) toast.success(`Пространство: ${space.name}`) @@ -64,7 +66,7 @@ export function SpaceSwitcher() { if (!n) return try { const created = await api.createSpace({ name: n }) - setStoredSpaceId(created.id) + setSpaceId(created.id) setCreateOpen(false) setName('') await qc.invalidateQueries({ queryKey: spacesKeys.all }) diff --git a/apps/web/src/components/vps-access-sheet.tsx b/apps/web/src/components/vps-access-sheet.tsx index b1e6d01..1b26ace 100644 --- a/apps/web/src/components/vps-access-sheet.tsx +++ b/apps/web/src/components/vps-access-sheet.tsx @@ -21,7 +21,7 @@ import { } from '@cfdm/ui/components/sheet' import { api } from '@/lib/api-client' -import { getStoredSpaceId } from '@/lib/space' +import { useSpaceId } from '@/lib/space' import { spacesQueryOptions, snapshotKeys } from '@/queries/snapshot' import type { Vps } from '@/types/entities' @@ -33,8 +33,9 @@ type Props = { export function VpsAccessSheet({ vps, open, onOpenChange }: Props) { const qc = useQueryClient() + const { spaceId } = useSpaceId() const { data: spaces = [] } = useQuery(spacesQueryOptions()) - const fromSpaceId = getStoredSpaceId() ?? spaces.find((s) => s.kind === 'main')?.id ?? '' + const fromSpaceId = spaceId ?? spaces.find((s) => s.kind === 'main')?.id ?? '' const targets = spaces.filter((s) => s.id !== fromSpaceId) const [toSpaceId, setToSpaceId] = useState('') const [permission, setPermission] = useState<'read' | 'write'>('read') diff --git a/apps/web/src/lib/space.ts b/apps/web/src/lib/space.ts index 8661237..53eeb1a 100644 --- a/apps/web/src/lib/space.ts +++ b/apps/web/src/lib/space.ts @@ -1,4 +1,15 @@ +import { + createContext, + createElement, + useCallback, + useContext, + useMemo, + useState, + type ReactNode, +} from 'react' + const STORAGE_KEY = 'vps_space_id' +const SPACE_EVENT = 'vps-space-changed' export type SpaceDto = { id: string @@ -21,6 +32,7 @@ export function getStoredSpaceId(): string | null { export function setStoredSpaceId(id: string): void { try { localStorage.setItem(STORAGE_KEY, id) + window.dispatchEvent(new CustomEvent(SPACE_EVENT, { detail: id })) } catch { /* ignore */ } @@ -29,7 +41,42 @@ export function setStoredSpaceId(id: string): void { export function clearStoredSpaceId(): void { try { localStorage.removeItem(STORAGE_KEY) + window.dispatchEvent(new CustomEvent(SPACE_EVENT, { detail: null })) } catch { /* ignore */ } } + +type SpaceContextValue = { + spaceId: string | null + setSpaceId: (id: string) => void +} + +const SpaceContext = createContext(null) + +/** Provides reactive spaceId so switcher / snapshot / pages re-render on change. */ +export function SpaceProvider({ children }: { children: ReactNode }) { + const [spaceId, setSpaceIdState] = useState(() => + getStoredSpaceId(), + ) + + const setSpaceId = useCallback((id: string) => { + setStoredSpaceId(id) + setSpaceIdState(id) + }, []) + + const value = useMemo( + () => ({ spaceId, setSpaceId }), + [spaceId, setSpaceId], + ) + + return createElement(SpaceContext.Provider, { value }, children) +} + +export function useSpaceId(): SpaceContextValue { + const ctx = useContext(SpaceContext) + if (!ctx) { + throw new Error('useSpaceId must be used within SpaceProvider') + } + return ctx +} diff --git a/apps/web/src/queries/snapshot.ts b/apps/web/src/queries/snapshot.ts index e2b1258..170e94a 100644 --- a/apps/web/src/queries/snapshot.ts +++ b/apps/web/src/queries/snapshot.ts @@ -7,11 +7,15 @@ export const snapshotKeys = { space: (spaceId: string | null) => ['snapshot', spaceId ?? 'default'] as const, } -export const snapshotQueryOptions = () => ({ - queryKey: snapshotKeys.space(getStoredSpaceId()), - queryFn: () => api.fetchData(), - staleTime: 30_000, -}) +/** Pass spaceId from useSpaceId() so queryKey updates on switch. */ +export const snapshotQueryOptions = (spaceId?: string | null) => { + const id = spaceId === undefined ? getStoredSpaceId() : spaceId + return { + queryKey: snapshotKeys.space(id), + queryFn: () => api.fetchData(), + staleTime: 30_000, + } +} export const spacesKeys = { all: ['spaces'] as const, diff --git a/apps/web/src/routes/_auth/spaces.tsx b/apps/web/src/routes/_auth/spaces.tsx index d4e36ce..e4bce75 100644 --- a/apps/web/src/routes/_auth/spaces.tsx +++ b/apps/web/src/routes/_auth/spaces.tsx @@ -26,7 +26,7 @@ import { PageHeader } from '@/components/page-header' import { PageShell } from '@/components/page-shell' import { QueryState } from '@/components/query-state' import { api } from '@/lib/api-client' -import { getStoredSpaceId } from '@/lib/space' +import { useSpaceId } from '@/lib/space' import { spacesKeys, spacesQueryOptions } from '@/queries/snapshot' export const Route = createFileRoute('/_auth/spaces')({ @@ -42,7 +42,7 @@ type MemberRow = { function SpacesPage() { const qc = useQueryClient() - const spaceId = getStoredSpaceId() + const { spaceId } = useSpaceId() const { data: spaces = [] } = useQuery(spacesQueryOptions()) const current = spaces.find((s) => s.id === spaceId) ?? spaces[0] const currentId = current?.id ?? ''