feat(tenancies-p5): sidebar entry + 404 top-level page + API module gate
- Dashboard layout resolves tenanciesModuleByPort server-side (one isTenanciesModuleEnabled call per port the user has access to) and passes the map through AppShell → Sidebar. Atomic SSR — no flicker of the nav entry in/out after hydration. - Sidebar gains NavItemGated.requiresTenanciesModule. The Tenancies entry (KeyRound icon, immediately below Berths) only renders when the currently-active port has the flag flipped on. Per-port live switch fires when the rep toggles ports without reload. - /[portSlug]/tenancies + /[portSlug]/tenancies/[id] both call isTenanciesModuleEnabled and notFound() when disabled — guards against direct URL access even when the sidebar is hidden. - API routes (/api/v1/tenancies, /[id], /berths/[id]/tenancies) prepended with assertTenanciesModuleEnabled — matches design § "All routes ... return 404 when off". NotFoundError maps to 404. - Existing tenancy API tests get a makePortWithTenancies() helper (calls enableTenanciesModule after makePort) so the gate is satisfied. Affects 2 test files (16 tests retargeted). Verified: tsc clean, 1493/1493 vitest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,9 @@ interface AppShellProps {
|
||||
/** Per-port logo URLs resolved server-side. Sidebar picks the entry
|
||||
* matching the currently-active port from the UI store. */
|
||||
portLogoUrls: Record<string, string | null>;
|
||||
/** Per-port `tenancies_module_enabled` resolution. Gates the Tenancies
|
||||
* sidebar entry SSR-side so the nav doesn't flicker in/out. */
|
||||
tenanciesModuleByPort: Record<string, boolean>;
|
||||
/**
|
||||
* Server-rendered form-factor hint (from the request User-Agent). The
|
||||
* shell mounts the matching tree on first render so we never paint the
|
||||
@@ -86,6 +89,7 @@ export function AppShell({
|
||||
user,
|
||||
ports,
|
||||
portLogoUrls,
|
||||
tenanciesModuleByPort,
|
||||
initialFormFactor,
|
||||
children,
|
||||
}: AppShellProps) {
|
||||
@@ -137,6 +141,7 @@ export function AppShell({
|
||||
user,
|
||||
ports,
|
||||
portLogoUrls,
|
||||
tenanciesModuleByPort,
|
||||
};
|
||||
|
||||
// Chrome subtree per tier.
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Users,
|
||||
Bookmark,
|
||||
Anchor,
|
||||
KeyRound,
|
||||
Ship,
|
||||
Building2,
|
||||
Receipt,
|
||||
@@ -51,6 +52,9 @@ interface SidebarProps {
|
||||
* The sidebar header swaps to the current port's logo via the UI
|
||||
* store's `currentPortId`. Null entries render the wordmark fallback. */
|
||||
portLogoUrls?: Record<string, string | null>;
|
||||
/** Per-port `tenancies_module_enabled` resolution. Gates the Tenancies
|
||||
* sidebar entry. Resolved server-side in the dashboard layout. */
|
||||
tenanciesModuleByPort?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
@@ -72,6 +76,12 @@ interface NavSection {
|
||||
umamiRequired?: boolean;
|
||||
}
|
||||
|
||||
interface NavItemGated extends NavItem {
|
||||
/** When true, only render this item if the tenancies module is enabled
|
||||
* for the current port. Resolved against `tenanciesModuleByPort`. */
|
||||
requiresTenanciesModule?: boolean;
|
||||
}
|
||||
|
||||
function buildNavSections(portSlug: string | undefined): NavSection[] {
|
||||
const base = portSlug ? `/${portSlug}` : '';
|
||||
|
||||
@@ -86,6 +96,12 @@ function buildNavSections(portSlug: string | undefined): NavSection[] {
|
||||
{ href: `${base}/companies`, label: 'Companies', icon: Building2 },
|
||||
{ href: `${base}/interests`, label: 'Interests', icon: Bookmark },
|
||||
{ href: `${base}/berths`, label: 'Berths', icon: Anchor },
|
||||
{
|
||||
href: `${base}/tenancies`,
|
||||
label: 'Tenancies',
|
||||
icon: KeyRound,
|
||||
requiresTenanciesModule: true,
|
||||
} as NavItemGated,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -235,6 +251,7 @@ function SidebarContent({
|
||||
hasAdminAccess,
|
||||
hasMarinaAccess,
|
||||
hasResidentialAccess,
|
||||
tenanciesModuleEnabled,
|
||||
user,
|
||||
ports,
|
||||
currentPort,
|
||||
@@ -248,6 +265,7 @@ function SidebarContent({
|
||||
hasAdminAccess: boolean;
|
||||
hasMarinaAccess: boolean;
|
||||
hasResidentialAccess: boolean;
|
||||
tenanciesModuleEnabled: boolean;
|
||||
user?: SidebarProps['user'];
|
||||
ports?: Port[];
|
||||
currentPort: Port | null;
|
||||
@@ -366,15 +384,22 @@ function SidebarContent({
|
||||
)}
|
||||
{(!section.adminRequired || adminExpanded || collapsed) && (
|
||||
<ul className="space-y-0.5">
|
||||
{section.items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<NavItemLink
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
active={isActive(item.href, item.exact)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{section.items
|
||||
.filter((item) => {
|
||||
const gated = item as NavItemGated;
|
||||
if (gated.requiresTenanciesModule && !tenanciesModuleEnabled)
|
||||
return false;
|
||||
return true;
|
||||
})
|
||||
.map((item) => (
|
||||
<li key={item.href}>
|
||||
<NavItemLink
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
active={isActive(item.href, item.exact)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<Separator className="mt-3 bg-slate-200" aria-hidden />
|
||||
@@ -456,6 +481,7 @@ export function Sidebar({
|
||||
user,
|
||||
ports,
|
||||
portLogoUrls,
|
||||
tenanciesModuleByPort,
|
||||
}: SidebarProps) {
|
||||
// Sidebar collapse removed - design preference is the always-expanded
|
||||
// form. Forcibly false; the store flag stays for backwards-compat with
|
||||
@@ -465,6 +491,9 @@ export function Sidebar({
|
||||
const currentPortId = useUIStore((s) => s.currentPortId);
|
||||
const currentPort = ports?.find((p) => p.id === currentPortId) ?? ports?.[0] ?? null;
|
||||
const currentLogoUrl = currentPortId ? (portLogoUrls?.[currentPortId] ?? null) : null;
|
||||
const tenanciesModuleEnabled = currentPortId
|
||||
? (tenanciesModuleByPort?.[currentPortId] ?? false)
|
||||
: false;
|
||||
|
||||
// Super admins see every section regardless of role rows.
|
||||
const hasAdminAccess =
|
||||
@@ -496,6 +525,7 @@ export function Sidebar({
|
||||
hasAdminAccess={hasAdminAccess}
|
||||
hasMarinaAccess={hasMarinaAccess}
|
||||
hasResidentialAccess={hasResidentialAccess}
|
||||
tenanciesModuleEnabled={tenanciesModuleEnabled}
|
||||
user={user}
|
||||
ports={ports}
|
||||
currentPort={currentPort}
|
||||
|
||||
Reference in New Issue
Block a user