From 18755a37ec1f446d452e44f5e5b98477a17a402c Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Thu, 13 Aug 2026 11:19:00 +0200 Subject: [PATCH] fix(underlag): render the workspace skeleton while the route loads (#1556) The extension loading.tsx branched on an x-pathname request header that middleware only ever set on the response, so the pathname always read empty, the fullscreen branch was dead code, and Dokumentinkorg loaded behind the old inline-shell silhouette. Branch on the client pathname instead (same convention as the dashboard loading.tsx), and extract the skeleton into one shared component used by both the route fallback and the workspace's client-fetch state, updated to the post-rebuild layout: full-bleed, no card wrapper, filter dropdown instead of the removed pill row. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 --- app/(dashboard)/e/[sector]/[slug]/loading.tsx | 66 ++++--------------- .../general/InvoiceInboxSkeleton.tsx | 48 ++++++++++++++ .../general/InvoiceInboxWorkspace.tsx | 52 ++------------- 3 files changed, 64 insertions(+), 102 deletions(-) create mode 100644 components/extensions/general/InvoiceInboxSkeleton.tsx diff --git a/app/(dashboard)/e/[sector]/[slug]/loading.tsx b/app/(dashboard)/e/[sector]/[slug]/loading.tsx index 929bde24..957208c6 100644 --- a/app/(dashboard)/e/[sector]/[slug]/loading.tsx +++ b/app/(dashboard)/e/[sector]/[slug]/loading.tsx @@ -1,25 +1,29 @@ -import { headers } from 'next/headers' +'use client' + +import { usePathname } from 'next/navigation' import { Skeleton } from '@/components/ui/skeleton' import { Card, CardHeader, CardContent } from '@/components/ui/card' import { PageHeader } from '@/components/ui/page-header' import { getExtensionDefinition } from '@/lib/extensions/sectors' +import InvoiceInboxSkeleton from '@/components/extensions/general/InvoiceInboxSkeleton' // Mirror of FULLSCREEN_WORKSPACES in ExtensionWorkspaceLoader. loading.tsx -// can't read route params, so we inspect the forwarded x-pathname header to -// branch the skeleton shape: the parent dashboard loading.tsx renders a -// metrics dashboard shape that has nothing to do with extension workspaces. +// can't read route params, so we branch on the client pathname: the parent +// dashboard loading.tsx renders a metrics dashboard shape that has nothing +// to do with extension workspaces. (This must stay a client component: the +// x-pathname header middleware sets goes on the response, so headers() in a +// server loading file never sees it and the fullscreen branch went dead.) const FULLSCREEN_WORKSPACES = new Set(['general/invoice-inbox']) -export default async function ExtensionWorkspaceLoading() { - const h = await headers() - const pathname = h.get('x-pathname') ?? '' +export default function ExtensionWorkspaceLoading() { + const pathname = usePathname() const match = pathname.match(/^\/e\/([^/]+)\/([^/]+)/) const sector = match?.[1] ?? '' const slug = match?.[2] ?? '' const key = `${sector}/${slug}` if (FULLSCREEN_WORKSPACES.has(key)) { - return + return } const definition = sector && slug ? getExtensionDefinition(sector, slug) : undefined @@ -101,49 +105,3 @@ function TicSkeleton() { ) } - -function FullScreenWorkspaceSkeleton() { - return ( -
-
-
-
- - - -
- -
-
- -
-
-
-
- ) -} diff --git a/components/extensions/general/InvoiceInboxSkeleton.tsx b/components/extensions/general/InvoiceInboxSkeleton.tsx new file mode 100644 index 00000000..3ce9a5a3 --- /dev/null +++ b/components/extensions/general/InvoiceInboxSkeleton.tsx @@ -0,0 +1,48 @@ +import { Skeleton } from '@/components/ui/skeleton' + +// Single source of truth for the Dokumentinkorg loading silhouette, rendered +// by both the route-level loading.tsx and the workspace's own client-fetch +// state so the route fallback, the fetch shell, and the loaded UI are one +// shape with no reflow. Mirrors the live layout: full-bleed top bar + +// 3-pane grid, list pane with search + filter-dropdown trigger (PR #1524 +// replaced the old filter-pill row with a single full-width dropdown). +export default function InvoiceInboxSkeleton() { + return ( +
+
+
+
+ + + +
+ +
+
+ +
+
+
+
+ ) +} diff --git a/components/extensions/general/InvoiceInboxWorkspace.tsx b/components/extensions/general/InvoiceInboxWorkspace.tsx index 5e084901..9feed7d8 100644 --- a/components/extensions/general/InvoiceInboxWorkspace.tsx +++ b/components/extensions/general/InvoiceInboxWorkspace.tsx @@ -52,6 +52,7 @@ import { cn, formatCurrency, formatDate, formatDateLong } from '@/lib/utils' import { QUIET_LINK_CLASS } from '@/components/ui/dry-table' import { GoogleMark, MicrosoftMark } from '@/components/ui/provider-marks' import EditKonteringDialog from '@/components/extensions/general/EditKonteringDialog' +import InvoiceInboxSkeleton from '@/components/extensions/general/InvoiceInboxSkeleton' import { WhatsAppMark } from '@/components/extensions/general/WhatsAppMark' import { useReceiptHunt } from '@/components/extensions/general/use-receipt-hunt' import { createClient } from '@/lib/supabase/client' @@ -302,55 +303,10 @@ function deriveInboxStatus(item: InboxItem): InboxStatus { } // ── Skeleton ───────────────────────────────────────────────── -// Mirrors the live layout (top bar + 3-pane card) so the transition from -// the route-level loading.tsx to data-loaded content has no visible reflow. -// Keep in sync with app/(dashboard)/e/[sector]/[slug]/loading.tsx. +// Shared with app/(dashboard)/e/[sector]/[slug]/loading.tsx so the route +// fallback and this client-fetch shell are one silhouette with no reflow. -function WorkspaceSkeleton() { - return ( -
-
-
-
- - - -
- -
-
- -
-
-
-
- ) -} +const WorkspaceSkeleton = InvoiceInboxSkeleton // ── Main component ───────────────────────────────────────────