* feat(PBI-76): extend UserSettings schema with layout Adds layout.splitPanePositions and layout.activeSprints. These will hold values currently kept in client-side and server-side cookies (Phase 2). Two new tests cover the shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(PBI-76): migrate SplitPane positions to user-settings store Outside of a drag the store is the source of truth (cross-tab updates flow in for free). During a drag we keep splits in local state so mousemove does not round-trip through the store. On mouseup we persist the final splits via setPref. Removes document.cookie reads/writes — cookieKey is reused as the store-key for backwards compat. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(PBI-76): resolveActiveSprint reads from User.settings lib/active-sprint: - New helpers: getActiveSprintIdFromSettings, setActiveSprintInSettings, clearActiveSprintInSettings — all read/write user.settings.layout.activeSprints. - resolveActiveSprint(productId, userId) — userId now required, falls back to first OPEN, then most recent CLOSED sprint. - Cookie helpers (getActiveSprintIdFromCookie/setActiveSprintCookie/ clearActiveSprintCookie) removed. Callers updated to pass session.userId. The cookie-based fallback path is gone — `actions/active-sprint.ts` and `actions/sprints.ts` will be updated in the next commit (T-917). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(PBI-76): rewrite setActiveSprint callers to use settings setActiveSprintAction, syncActiveSprintCookieAction, and the two sprint-creation paths in actions/sprints.ts now write through setActiveSprintInSettings (which also emits pg_notify for cross-tab sync) instead of dropping a cookie. The action names keep the 'cookie' suffix in the user-visible API for now — clean rename can come later. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(PBI-76): migration helper v2 — handle legacy cookies Bumps marker version to 'v2'. buildMigrationPatch now also scans document.cookie for `sp:*` (split-pane positions) and `active_sprint_*` (active sprint per product) and lifts them into layout.splitPanePositions / layout.activeSprints. clearLegacyStorage replaces clearLegacyLocalStorage and clears both keys and cookies. clearLegacyLocalStorage stays as a deprecated alias so the bridge upgrade is a single rename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(PBI-76): align tests with new SplitPane and active-sprint flow - split-pane.test.tsx: seed positions via Zustand store instead of document.cookie; mock @/actions/user-settings so the prisma client is not transitively initialised in jsdom. - backlog-split-pane.test.tsx: same action mock. - sprint-dates.test.ts: add user.findUnique/update + $executeRaw mocks because createSprintAction now writes to user-settings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
import { redirect } from 'next/navigation'
|
|
import { requireSession } from '@/lib/auth-guard'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { productAccessFilter } from '@/lib/product-access'
|
|
import { resolveActiveSprint } from '@/lib/active-sprint'
|
|
import { NavBar } from '@/components/shared/nav-bar'
|
|
import { MinWidthBanner } from '@/components/shared/min-width-banner'
|
|
import { StatusBar } from '@/components/shared/status-bar'
|
|
import { SoloRealtimeBridge } from '@/components/solo/realtime-bridge'
|
|
import { NotificationsBridge } from '@/components/notifications/notifications-bridge'
|
|
import { UserSettingsBridge } from '@/components/shared/user-settings-bridge'
|
|
import { parseUserSettings } from '@/lib/user-settings'
|
|
import { AlertToast } from '@/components/shared/alert-toast'
|
|
import { Suspense } from 'react'
|
|
|
|
export default async function AppLayout({ children }: { children: React.ReactNode }) {
|
|
const session = await requireSession()
|
|
|
|
const [user, userRoles, accessibleProducts] = await Promise.all([
|
|
prisma.user.findUnique({
|
|
where: { id: session.userId },
|
|
select: { username: true, email: true, active_product_id: true, min_quota_pct: true, settings: true },
|
|
}),
|
|
prisma.userRole.findMany({
|
|
where: { user_id: session.userId },
|
|
select: { role: true },
|
|
}),
|
|
prisma.product.findMany({
|
|
where: { archived: false, ...productAccessFilter(session.userId) },
|
|
orderBy: { name: 'asc' },
|
|
select: { id: true, name: true },
|
|
}),
|
|
])
|
|
const roles = userRoles.map(r => r.role as string)
|
|
|
|
if (!user) {
|
|
redirect('/login')
|
|
}
|
|
|
|
// Resolve active product — clear stale reference if archived or inaccessible
|
|
let activeProduct: { id: string; name: string } | null = null
|
|
let hasActiveSprint = false
|
|
if (user.active_product_id) {
|
|
const product = await prisma.product.findFirst({
|
|
where: { id: user.active_product_id, archived: false, ...productAccessFilter(session.userId) },
|
|
select: { id: true, name: true },
|
|
})
|
|
if (product) {
|
|
activeProduct = product
|
|
const resolved = await resolveActiveSprint(product.id, session.userId)
|
|
hasActiveSprint = !!resolved
|
|
} else {
|
|
await prisma.user.update({
|
|
where: { id: session.userId },
|
|
data: { active_product_id: null },
|
|
})
|
|
redirect('/dashboard?alert=product_unavailable')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="h-screen bg-background flex flex-col overflow-hidden">
|
|
<a href="#main-content" className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-md focus:text-sm">
|
|
Ga naar inhoud
|
|
</a>
|
|
<NavBar
|
|
isDemo={session.isDemo}
|
|
roles={roles}
|
|
userId={session.userId}
|
|
username={user.username}
|
|
email={user.email}
|
|
activeProduct={activeProduct}
|
|
products={accessibleProducts}
|
|
hasActiveSprint={hasActiveSprint}
|
|
minQuotaPct={user.min_quota_pct}
|
|
/>
|
|
<MinWidthBanner />
|
|
<main id="main-content" className="flex-1 flex flex-col overflow-y-auto min-h-0">
|
|
{children}
|
|
</main>
|
|
<StatusBar />
|
|
<SoloRealtimeBridge productId={activeProduct?.id ?? null} />
|
|
<NotificationsBridge userId={session.userId} />
|
|
<UserSettingsBridge
|
|
initial={parseUserSettings(user.settings)}
|
|
isDemo={session.isDemo ?? false}
|
|
/>
|
|
<Suspense>
|
|
<AlertToast />
|
|
</Suspense>
|
|
</div>
|
|
)
|
|
}
|