-
Notifications
You must be signed in to change notification settings - Fork 0
feat(console): real-mode nav/route gating to backend-backed screens #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,29 +3,17 @@ | |
| * (workspace switcher, ⌘K search, live network badge, mask-eye, bell, avatar), | ||
| * and the routed content area with the cursor-interactive canvas behind the cards. | ||
| */ | ||
| import { | ||
| ArrowUpRight, | ||
| Bell, | ||
| ChevronDown, | ||
| Eye, | ||
| EyeOff, | ||
| FileText, | ||
| LayoutDashboard, | ||
| ScrollText, | ||
| Settings, | ||
| ShieldCheck, | ||
| Users, | ||
| Wallet, | ||
| CheckCheck, | ||
| } from "lucide-react"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { Bell, CheckCheck, ChevronDown, Eye, EyeOff, Settings, Users } from "lucide-react"; | ||
| import { Fragment, useEffect, useRef, useState } from "react"; | ||
| import { AnimatePresence, motion } from "framer-motion"; | ||
| import { NavLink, Route, Routes, useLocation, useNavigate } from "react-router-dom"; | ||
| import { NavLink, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; | ||
| import { StageVideo } from "../ui/StageVideo"; | ||
| import { CommandBar } from "./CommandBar"; | ||
| import { AvalancheMark, Logo } from "../ui/Logo"; | ||
| import { NetworkMenu } from "./NetworkMenu"; | ||
| import { useConsole } from "../lib/store"; | ||
| import { DEMO_MODE } from "../demo/flag"; | ||
| import { REAL_HOME, visibleNavGroups, visibleNavItems } from "./nav"; | ||
| import { formatAddress } from "../lib/format"; | ||
| import { Dashboard } from "../screens/Dashboard"; | ||
| import { Approvals } from "../screens/Approvals"; | ||
|
|
@@ -213,21 +201,28 @@ export function Shell() { | |
| {/* body: sidebar nav + routed content */} | ||
| <div className="flex flex-1 overflow-hidden"> | ||
| <aside className="flex w-[240px] flex-none flex-col gap-1 border-r border-border bg-surface px-3.5 py-4"> | ||
| <Eyebrow>Overview</Eyebrow> | ||
| <NavItem to="/" icon={LayoutDashboard} label="Overview" /> | ||
| <Eyebrow>Payments</Eyebrow> | ||
| <NavItem to="/contractors" icon={Users} label="Contractors" /> | ||
| <NavItem to="/payroll" icon={Users} label="Payroll" /> | ||
| <NavItem to="/invoices" icon={FileText} label="Invoices" /> | ||
| <NavItem to="/pay" icon={ArrowUpRight} label="One-off payment" /> | ||
| <Eyebrow>Operations</Eyebrow> | ||
| <NavItem to="/approvals" icon={CheckCheck} label="Approvals" badge={pending || undefined} /> | ||
| <NavItem to="/treasury" icon={Wallet} label="Treasury" /> | ||
| <Eyebrow>Compliance</Eyebrow> | ||
| <NavItem to="/grants" icon={ShieldCheck} label="Auditor access" /> | ||
| <NavItem to="/audit" icon={ScrollText} label="Audit log" /> | ||
| {visibleNavGroups().map((group) => ( | ||
| <Fragment key={group}> | ||
| <Eyebrow>{group}</Eyebrow> | ||
| {visibleNavItems() | ||
| .filter((item) => !item.footer && item.group === group) | ||
| .map((item) => ( | ||
| <NavItem | ||
| key={item.to} | ||
| to={item.to} | ||
| icon={item.icon} | ||
| label={item.label} | ||
| badge={item.to === "/approvals" ? pending || undefined : undefined} | ||
| /> | ||
| ))} | ||
| </Fragment> | ||
| ))} | ||
| <div className="flex-1" /> | ||
| <NavItem to="/settings" icon={Settings} label="Settings & team" /> | ||
| {visibleNavItems() | ||
| .filter((item) => item.footer) | ||
| .map((item) => ( | ||
| <NavItem key={item.to} to={item.to} icon={item.icon} label={item.label} /> | ||
| ))} | ||
| <div className="mt-auto flex items-center justify-center gap-1.5 border-t border-border/60 pt-3 text-[11px] font-medium text-[#8a9099]" data-testid="built-on-avalanche"> | ||
| <AvalancheMark size={13} /> Built on Avalanche | ||
| </div> | ||
|
|
@@ -237,18 +232,30 @@ export function Shell() { | |
| <main className="no-scrollbar relative z-10 h-full overflow-y-auto px-5 py-6"> | ||
| {activeOrg ? ( | ||
| <Routes location={loc} key={loc.pathname}> | ||
| <Route path="/" element={<Dashboard />} /> | ||
| <Route path="/approvals" element={<Approvals />} /> | ||
| <Route path="/contractors" element={<Contractors />} /> | ||
| <Route path="/payroll" element={<Payroll />} /> | ||
| <Route path="/invoices" element={<Invoices />} /> | ||
| <Route path="/pay" element={<Pay />} /> | ||
| <Route path="/treasury" element={<Treasury />} /> | ||
| <Route path="/grants" element={<Grants />} /> | ||
| <Route path="/audit" element={<AuditLog />} /> | ||
| <Route path="/claim" element={<InviteClaim />} /> | ||
| <Route path="/settings" element={<SettingsScreen />} /> | ||
| <Route path="*" element={<Dashboard />} /> | ||
| {DEMO_MODE ? ( | ||
| <> | ||
| <Route path="/" element={<Dashboard />} /> | ||
| <Route path="/approvals" element={<Approvals />} /> | ||
| <Route path="/contractors" element={<Contractors />} /> | ||
| <Route path="/payroll" element={<Payroll />} /> | ||
| <Route path="/invoices" element={<Invoices />} /> | ||
| <Route path="/pay" element={<Pay />} /> | ||
| <Route path="/treasury" element={<Treasury />} /> | ||
| <Route path="/grants" element={<Grants />} /> | ||
| <Route path="/audit" element={<AuditLog />} /> | ||
| <Route path="/claim" element={<InviteClaim />} /> | ||
| <Route path="/settings" element={<SettingsScreen />} /> | ||
| <Route path="*" element={<Dashboard />} /> | ||
| </> | ||
| ) : ( | ||
|
Comment on lines
+235
to
+250
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This branch makes the demo-only routes unreachable in real mode, but the demo screens are still statically imported at module load. A real build can still include and evaluate those screen modules and their transitive dependencies, so the intended real-mode bundle split does not happen from this route gate alone. Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/console/src/app/Shell.tsx
Line: 235-250
Comment:
**Demo Screens Still Imported**
This branch makes the demo-only routes unreachable in real mode, but the demo screens are still statically imported at module load. A real build can still include and evaluate those screen modules and their transitive dependencies, so the intended real-mode bundle split does not happen from this route gate alone.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| <> | ||
| {/* Real mode: only the backend-backed screens; everything else | ||
| is demo-only and redirects to the treasury landing. */} | ||
| <Route path="/payroll" element={<Payroll />} /> | ||
| <Route path="/treasury" element={<Treasury />} /> | ||
| <Route path="*" element={<Navigate to={REAL_HOME} replace />} /> | ||
| </> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a real-mode user with an active org opens Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/console/src/app/Shell.tsx
Line: 257
Comment:
**Invite Claim Route Redirected**
When a real-mode user with an active org opens `/claim`, this wildcard catches the URL and sends them to `/treasury`. The previous route table registered `InviteClaim`, so invite links can no longer reach the claim screen in the real build.
How can I resolve this? If you propose a fix, please make it concise. |
||
| )} | ||
| </Routes> | ||
| ) : ( | ||
| <NoOrgPlaceholder address={session?.user.address} /> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { NAV_ITEMS, REAL_HOME, visibleNavGroups, visibleNavItems } from "./nav"; | ||
|
|
||
| describe("real-mode nav gating", () => { | ||
| it("real mode exposes only the backend-backed screens (Payroll + Treasury)", () => { | ||
| const tos = visibleNavItems(false) | ||
| .map((i) => i.to) | ||
| .sort(); | ||
| expect(tos).toEqual(["/payroll", "/treasury"]); | ||
| }); | ||
|
|
||
| it("real mode hides every no-backend screen", () => { | ||
| const tos = new Set(visibleNavItems(false).map((i) => i.to)); | ||
| for (const hidden of ["/", "/contractors", "/invoices", "/pay", "/approvals", "/grants", "/audit", "/settings"]) { | ||
| expect(tos.has(hidden)).toBe(false); | ||
| } | ||
| }); | ||
|
|
||
| it("demo mode shows the full product", () => { | ||
| const items = visibleNavItems(true); | ||
| expect(items).toHaveLength(NAV_ITEMS.length); | ||
| expect(items.some((i) => i.to === "/contractors")).toBe(true); | ||
| expect(items.some((i) => i.to === "/settings")).toBe(true); | ||
| }); | ||
|
|
||
| it("real-mode groups collapse to the two wired sections, in order", () => { | ||
| expect(visibleNavGroups(false)).toEqual(["Payments", "Operations"]); | ||
| }); | ||
|
|
||
| it("demo-mode keeps all non-footer sections", () => { | ||
| expect(visibleNavGroups(true)).toEqual(["Overview", "Payments", "Operations", "Compliance"]); | ||
| }); | ||
|
|
||
| it("real mode lands on the treasury (Overview has no backend)", () => { | ||
| expect(REAL_HOME).toBe("/treasury"); | ||
| // the landing must itself be a real-backed screen | ||
| const landing = NAV_ITEMS.find((i) => i.to === REAL_HOME); | ||
| expect(landing?.realBacked).toBe(true); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /** | ||
| * Nav model — the single source of truth for the sidebar + command bar, and which | ||
| * screens are exposed in each build mode. In **real mode** the console only shows | ||
| * screens that actually talk to the eERC backend (auth/onboarding/treasury/payroll); | ||
| * the rest (Overview, Contractors, Invoices, one-off Pay, Approvals, Grants, Audit, | ||
| * Settings) have no backend on this platform and stay **demo-only**. Demo mode | ||
|
Comment on lines
+5
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Keep backed organization and team settings reachable in real mode.
Also applies to: 36-36 🤖 Prompt for AI Agents |
||
| * (`VITE_DEMO_MODE=1`) shows the full product as the vision. | ||
| */ | ||
| import type { LucideIcon } from "lucide-react"; | ||
| import { ArrowUpRight, CheckCheck, FileText, LayoutDashboard, ScrollText, Settings, ShieldCheck, Users, Wallet } from "lucide-react"; | ||
| import { DEMO_MODE } from "../demo/flag"; | ||
|
|
||
| export type NavGroup = "Overview" | "Payments" | "Operations" | "Compliance" | "Settings"; | ||
|
|
||
| export interface NavItemDef { | ||
| to: string; | ||
| label: string; | ||
| icon: LucideIcon; | ||
| group: NavGroup; | ||
| /** True only when the screen is wired to a real backend endpoint. */ | ||
| realBacked: boolean; | ||
| /** Pinned to the bottom of the sidebar (Settings). */ | ||
| footer?: boolean; | ||
| } | ||
|
|
||
| export const NAV_ITEMS: NavItemDef[] = [ | ||
| { to: "/", label: "Overview", icon: LayoutDashboard, group: "Overview", realBacked: false }, | ||
| { to: "/contractors", label: "Contractors", icon: Users, group: "Payments", realBacked: false }, | ||
| { to: "/payroll", label: "Payroll", icon: Users, group: "Payments", realBacked: true }, | ||
| { to: "/invoices", label: "Invoices", icon: FileText, group: "Payments", realBacked: false }, | ||
| { to: "/pay", label: "One-off payment", icon: ArrowUpRight, group: "Payments", realBacked: false }, | ||
| { to: "/approvals", label: "Approvals", icon: CheckCheck, group: "Operations", realBacked: false }, | ||
| { to: "/treasury", label: "Treasury", icon: Wallet, group: "Operations", realBacked: true }, | ||
| { to: "/grants", label: "Auditor access", icon: ShieldCheck, group: "Compliance", realBacked: false }, | ||
| { to: "/audit", label: "Audit log", icon: ScrollText, group: "Compliance", realBacked: false }, | ||
| { to: "/settings", label: "Settings & team", icon: Settings, group: "Settings", realBacked: false, footer: true }, | ||
| ]; | ||
|
|
||
| /** Where real mode lands (Overview has no backend). */ | ||
| export const REAL_HOME = "/treasury"; | ||
|
|
||
| /** Nav items to render for the given build mode. Real mode hides no-backend screens. */ | ||
| export function visibleNavItems(demoMode: boolean = DEMO_MODE): NavItemDef[] { | ||
| return NAV_ITEMS.filter((item) => demoMode || item.realBacked); | ||
| } | ||
|
|
||
| /** Ordered, de-duplicated list of groups that have at least one visible non-footer item. */ | ||
| export function visibleNavGroups(demoMode: boolean = DEMO_MODE): NavGroup[] { | ||
| const groups: NavGroup[] = []; | ||
| for (const item of visibleNavItems(demoMode)) { | ||
| if (item.footer) continue; | ||
| if (!groups.includes(item.group)) groups.push(item.group); | ||
| } | ||
| return groups; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Derive palette destinations from the shared navigation model.
This duplicates both route definitions and real-mode visibility metadata instead of using
visibleNavItems(). It can drift from the sidebar and route-gating contract; keep any command-specific labels as metadata inNavItemDefif needed.Suggested direction
📝 Committable suggestion
🤖 Prompt for AI Agents