ca53322b14
Maximum update depth exceeded showed на /dictionaries/$name когда RecordDrawer
открывался в create mode. Root cause:
1. SchemaDrivenForm useEffect deps включал onCounterChange/onDirtyChange.
2. Parent route передавал inline arrow (() => setDrawerCount({...})) —
новая identity каждый render.
3. setDrawerCount({visible, total}) создавал новый object literal даже
при тех же значениях → React re-render → effect fires → loop.
Fix:
- SchemaDrivenForm: ref-pattern для callback. useEffect subscribed только
на value deps (visibleFieldCount/totalFieldCount/dirtyCount), latest
callback хранится в ref.
- dataValues — useMemo с frozen EMPTY_DATA default чтобы не invalidate'ить
isFieldVisible memo каждый render.
Plus:
- GlobeLoader component — orbital globe spinner per Downloads/Globe loader
prototype. Pure SVG + CSS animations, без d3/topojson.
- LoadingBlock переключен с Loader2 на GlobeLoader (все usage обновляется
автоматически: RecordHistoryDrawer, dictionaries.$name editor loading,
search results, webhooks list, etc).
- AppErrorBoundary — friendly fallback вместо TanStack DefaultErrorComponent.
GlobeLoader (paused/dimmed) + 'Что-то пошло не так' + [Обновить][На главную].
Dev-only collapsible с raw stack trace.
43 lines
1.2 KiB
TypeScript
43 lines
1.2 KiB
TypeScript
import { cn } from '@/lib/utils'
|
|
import { GlobeLoader } from './globe-loader'
|
|
|
|
/**
|
|
* LoadingBlock — central spinner с подписью. Используется везде где
|
|
* загружаются данные / окна. С 11.05.2026 заменили Loader2 spinner на
|
|
* GlobeLoader (orbital globe loader per /Users/zimin/Downloads/Globe loader).
|
|
*
|
|
* Size variants:
|
|
* - sm: small inline (py-4, 32px globe)
|
|
* - md: default centered (py-12, 64px globe)
|
|
* - lg: prominent full-page (py-20, 96px globe)
|
|
*/
|
|
export type LoadingBlockProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
label?: React.ReactNode
|
|
size?: 'sm' | 'md' | 'lg'
|
|
}
|
|
|
|
const SIZES = {
|
|
sm: { wrapper: 'py-4', globe: 32 },
|
|
md: { wrapper: 'py-12', globe: 64 },
|
|
lg: { wrapper: 'py-20', globe: 96 },
|
|
} as const
|
|
|
|
export function LoadingBlock({ className, label, size = 'md', ...props }: LoadingBlockProps) {
|
|
const s = SIZES[size]
|
|
return (
|
|
<div
|
|
role="status"
|
|
aria-busy="true"
|
|
className={cn(
|
|
'flex flex-col items-center justify-center gap-3 text-ink',
|
|
s.wrapper,
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
<GlobeLoader size={s.globe} />
|
|
{label && <span className="text-body text-mute">{label}</span>}
|
|
</div>
|
|
)
|
|
}
|