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 ?? ''