fix(dashboards): prevent render loops with stable ready handlers and idempotent updates; improve filter hydration guards

fix(export): return 501 with hint when Playwright browsers missing; nicer error toast in UI

fix(site-header): export primary/secondary buttons as named for SC safety; keep static props for compat

fix(portal): add DialogDescription for a11y; tidy preview dialog

fix(csats): avoid reinit state loops with timestamp guard

chore(prisma): default dev DB to prisma/db.dev.sqlite and log path

chore(auth): add dev bypass flags wiring (server/client) for local testing

dev: seed script for Convex demo data
This commit is contained in:
Esdras Renan 2025-11-06 00:01:45 -03:00
parent ff0254df18
commit b62e14d8eb
13 changed files with 210 additions and 103 deletions

View file

@ -441,6 +441,14 @@ function filtersEqual(a: DashboardFilters, b: DashboardFilters) {
)
}
function deepEqual<T>(a: T, b: T) {
try {
return JSON.stringify(a) === JSON.stringify(b)
} catch {
return false
}
}
const widgetConfigSchema = z.object({
title: z.string().min(1, "Informe um título"),
type: z.string(),
@ -500,12 +508,14 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
useEffect(() => {
if (!detail) return
setDashboard(detail.dashboard)
setWidgets(detail.widgets)
setShares(detail.shares)
setDashboard((prev) => (prev && deepEqual(prev, detail.dashboard) ? prev : detail.dashboard))
setWidgets((prev) => (deepEqual(prev, detail.widgets) ? prev : detail.widgets))
setShares((prev) => (deepEqual(prev, detail.shares) ? prev : detail.shares))
const nextFilters = normalizeFilters(detail.dashboard.filters)
filtersHydratingRef.current = true
setFilters(nextFilters)
if (!filtersEqual(filters, nextFilters)) {
filtersHydratingRef.current = true
setFilters(nextFilters)
}
const syncedLayout = syncLayoutWithWidgets(layoutRef.current, detail.widgets, detail.dashboard.layout)
if (!layoutItemsEqual(layoutRef.current, syncedLayout)) {
layoutRef.current = syncedLayout
@ -543,6 +553,30 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
return new Set(currentSection?.widgetKeys ?? [])
}, [enforceTv, sections, activeSectionIndex])
// Ready handlers (stable per widget) defined before usage in canvasItems
const handleWidgetReady = useCallback((key: string, ready: boolean) => {
setReadyWidgets((prev) => {
const currentlyReady = prev.has(key)
if (currentlyReady === ready) return prev
const next = new Set(prev)
if (ready) next.add(key)
else next.delete(key)
return next
})
}, [])
const readyHandlersRef = useRef(new Map<string, (ready: boolean) => void>())
const getReadyHandler = useCallback(
(key: string) => {
const cached = readyHandlersRef.current.get(key)
if (cached) return cached
const handler = (ready: boolean) => handleWidgetReady(key, ready)
readyHandlersRef.current.set(key, handler)
return handler
},
[handleWidgetReady],
)
const canvasItems = packedLayout
.map((item) => {
const widget = widgetMap.get(item.i)
@ -565,7 +599,7 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
onDuplicate={() => handleDuplicateWidget(widget)}
onRemove={() => handleRemoveWidget(widget)}
onViewData={() => setDataTarget(widget)}
onReadyChange={(ready) => handleWidgetReady(widget.widgetKey, ready)}
onReadyChange={getReadyHandler(widget.widgetKey)}
/>
),
...(item.minW !== undefined ? { minW: item.minW } : {}),
@ -576,18 +610,6 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
const allWidgetsReady = canvasItems.length > 0 && canvasItems.every((item) => readyWidgets.has(item.key))
const handleWidgetReady = useCallback((key: string, ready: boolean) => {
setReadyWidgets((prev) => {
const next = new Set(prev)
if (ready) {
next.add(key)
} else {
next.delete(key)
}
return next
})
}, [])
useEffect(() => {
const keys = new Set(canvasItems.map((item) => item.key))
setReadyWidgets((prev) => {
@ -799,7 +821,13 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
}),
})
if (!response.ok) {
throw new Error("Export request failed")
let msg = "Export request failed"
try {
const data = await response.json()
if (data?.hint) msg = `${msg}: ${data.hint}`
else if (data?.error) msg = `${msg}: ${data.error}`
} catch {}
throw new Error(msg)
}
const blob = await response.blob()
const url = URL.createObjectURL(blob)
@ -811,7 +839,10 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
toast.success("Exportação gerada com sucesso!")
} catch (error) {
console.error(error)
toast.error("Não foi possível exportar o dashboard.")
const message = error instanceof Error ? error.message : String(error)
toast.error(message.includes("Playwright browsers not installed") || message.includes("npx playwright install")
? "Exportação indisponível em DEV: rode `npx playwright install` para habilitar."
: "Não foi possível exportar o dashboard.")
} finally {
setIsExporting(false)
}