fix(web): реактивно переключать пространство через SpaceProvider
Docker / build (push) Failing after 18s

localStorage не вызывал re-render; useSpaceId + remount main по spaceId.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denozordec
2026-07-18 16:15:52 +07:00
co-authored by Cursor
parent a61b352f0a
commit c90b42c9c4
6 changed files with 84 additions and 17 deletions
+14 -1
View File
@@ -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 (
<SpaceProvider>
<TooltipProvider delay={0}>
<SidebarProvider
style={
@@ -247,11 +249,22 @@ export function AppShell({ children }: { children: ReactNode }) {
</div>
</header>
<main className="flex flex-1 flex-col gap-4 px-4 py-4 md:gap-6 md:px-6 md:py-5">
{children}
<SpaceScopedMain>{children}</SpaceScopedMain>
</main>
</SidebarInset>
<GlobalSearch open={searchOpen} onOpenChange={setSearchOpen} />
</SidebarProvider>
</TooltipProvider>
</SpaceProvider>
)
}
/** Remount route content when space changes so snapshot queries pick new X-Space-Id. */
function SpaceScopedMain({ children }: { children: ReactNode }) {
const { spaceId } = useSpaceId()
return (
<div key={spaceId ?? 'default'} className="flex flex-1 flex-col gap-4 md:gap-6">
{children}
</div>
)
}
@@ -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 })
+3 -2
View File
@@ -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')
+47
View File
@@ -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<SpaceContextValue | null>(null)
/** Provides reactive spaceId so switcher / snapshot / pages re-render on change. */
export function SpaceProvider({ children }: { children: ReactNode }) {
const [spaceId, setSpaceIdState] = useState<string | null>(() =>
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
}
+9 -5
View File
@@ -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,
+2 -2
View File
@@ -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 ?? ''