diff --git a/app/(dashboard)/byra/kpi/page.tsx b/app/(dashboard)/byra/kpi/page.tsx index f0c903f9..54a6b526 100644 --- a/app/(dashboard)/byra/kpi/page.tsx +++ b/app/(dashboard)/byra/kpi/page.tsx @@ -1,8 +1,7 @@ import { redirect } from 'next/navigation' import { getTranslations } from 'next-intl/server' -import { TrendingUp } from 'lucide-react' import { PageHeader } from '@/components/ui/page-header' -import { EmptyState } from '@/components/ui/empty-state' +import { EmptyByraClients } from '@/components/ui/empty-state' import { fetchByraKpiOverview } from '@/lib/byra/kpi-overview' import ByraKpiView from '@/components/byra/ByraKpiView' import { getDashboardAuthContext } from '../../request-context' @@ -46,11 +45,7 @@ export default async function ByraKpiPage({
{overview.allClients.length === 0 ? ( - + ) : ( { + it('is only passed from client components', () => { + const files = [ + ...collectTsxFiles(path.join(repoRoot, 'app')), + ...collectTsxFiles(path.join(repoRoot, 'components')), + ] + + const offenders = files.filter((file) => { + const source = fs.readFileSync(file, 'utf8') + if (isClientComponent(source)) return false + // `[^>]*` keeps the match inside a single JSX element, newlines included. + return /]*\bicon=\{/.test(source) + }) + + expect(offenders.map((f) => path.relative(repoRoot, f))).toEqual([]) + }) +}) + +describe('byrå KPI empty state', () => { + const pagePath = path.join(repoRoot, 'app', '(dashboard)', 'byra', 'kpi', 'page.tsx') + + it('renders the preset from the server page instead of passing an icon', () => { + const source = fs.readFileSync(pagePath, 'utf8') + expect(isClientComponent(source)).toBe(false) + expect(source).toMatch(//) + expect(source).not.toMatch(/from 'lucide-react'/) + }) + + it('keeps the icon and the copy in the preset', () => { + const source = fs.readFileSync(path.join(repoRoot, 'components', 'ui', 'empty-state.tsx'), 'utf8') + expect(source).toMatch(/export function EmptyByraClients\(\)/) + expect(source).toMatch(/icon=\{TrendingUp\}/) + // Variable-agnostic: the preset reads from the `byra` namespace under its + // own translator name, so assert the keys, not the caller's identifier. + expect(source).toMatch(/\w+\('kpi_empty_title'\)/) + expect(source).toMatch(/\w+\('kpi_empty_description'\)/) + }) +}) diff --git a/components/ui/empty-state.tsx b/components/ui/empty-state.tsx index b7a32f5c..02b62947 100644 --- a/components/ui/empty-state.tsx +++ b/components/ui/empty-state.tsx @@ -13,6 +13,7 @@ import { FileText, Calendar, Plus, + TrendingUp, type LucideIcon, } from 'lucide-react' import { SupportLink } from '@/components/ui/support-link' @@ -185,3 +186,25 @@ export function EmptyReports() { /> ) } + +/** + * Byrå cockpit: no client companies yet. A preset, not a bare , because the only caller is a Server Component: a + * lucide icon is a forwardRef object that cannot cross the RSC boundary as a + * prop, while a reference to this client component can. The copy lives in the + * byra namespace, where the byrå surfaces already keep it. + */ +export function EmptyByraClients() { + // Named tByra, not t: this is the only preset here that reads from a + // namespace other than `empty`, and i18n/__tests__/message-keys.test.ts maps + // one variable name to one namespace per file. Reusing `t` would silently + // re-point every other preset's key in this file at `byra`. + const tByra = useTranslations('byra') + return ( + + ) +}