fix(web): реактивно переключать пространство через SpaceProvider
Docker / build (push) Failing after 18s
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:
@@ -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 })
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? ''
|
||||
|
||||
Reference in New Issue
Block a user