Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions client/src/pages/Calendar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import PageHeader from '../components/PageHeader';
import TabPills from '../components/ui/TabPills';
import { useValidTab } from '../hooks/useValidTab';
import useUrlParams from '../hooks/useUrlParams';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

import AgendaTab from '../components/calendar/AgendaTab';
import DayView from '../components/calendar/DayView';
Expand All @@ -17,18 +18,26 @@ import ReviewTab from '../components/calendar/ReviewTab';
import CalendarLifetimeTab from '../components/meatspace/tabs/CalendarTab';
import SyncTab from '../components/calendar/SyncTab';

// Exported so the nav-manifest tab-coverage guard (server/lib/navManifest.test.js)
// can assert each tab round-trips to a NAV_COMMANDS path.
export const TABS = [
{ id: 'agenda', label: 'Agenda', icon: CalendarDays },
{ id: 'day', label: 'Day', icon: CalendarIcon },
{ id: 'week', label: 'Week', icon: Columns },
{ id: 'month', label: 'Month', icon: LayoutGrid },
{ id: 'lifetime', label: 'Lifetime', icon: Clock },
{ id: 'review', label: 'Review', icon: ClipboardList },
{ id: 'sync', label: 'Sync', icon: RefreshCw },
{ id: 'config', label: 'Config', icon: Settings }
];
// Icon (and any other presentation-only detail) per tab id. The manifest
// (`tabGroup: 'calendar'`) owns id/label/order — this page owns only how each
// tab looks. Throws at import time if the manifest and this map drift, so a
// new manifest tab can't ship silently unreachable from this page's tab bar.
const TAB_PRESENTATION = {
agenda: { icon: CalendarDays },
day: { icon: CalendarIcon },
week: { icon: Columns },
month: { icon: LayoutGrid },
lifetime: { icon: Clock },
review: { icon: ClipboardList },
sync: { icon: RefreshCw },
config: { icon: Settings },
};

export const TABS = getPageNavTabs('calendar').map((tab) => {
const presentation = TAB_PRESENTATION[tab.id];
if (!presentation) throw new Error(`Calendar: no tab presentation for manifest tab "${tab.id}"`);
return { ...tab, ...presentation };
});

export default function Calendar() {
const navigate = useNavigate();
Expand Down
16 changes: 16 additions & 0 deletions client/src/pages/Calendar.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest';
import { TABS } from './Calendar';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

// Calendar derives its tab bar from the nav manifest's `tabGroup: 'calendar'`
// (#6365) — this pins that TABS stays in sync (id, label, declaration order)
// and that every manifest tab has a presentation entry (icon) in Calendar.jsx,
// which would otherwise only surface as a thrown import-time error.
describe('Calendar TABS ↔ nav manifest', () => {
it('derives every tab, in order, from the "calendar" tabGroup with a presentation entry', () => {
const manifestTabs = getPageNavTabs('calendar');
expect(TABS.map((t) => t.id)).toEqual(manifestTabs.map((t) => t.id));
expect(TABS.map((t) => t.label)).toEqual(manifestTabs.map((t) => t.label));
expect(TABS.every((t) => typeof t.icon === 'function' || typeof t.icon === 'object')).toBe(true);
});
});
20 changes: 15 additions & 5 deletions client/src/pages/Goals.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,24 @@ import PageHeader from '../components/PageHeader';
import TabPills from '../components/ui/TabPills';
import PageSkeleton from '../components/ui/PageSkeleton';
import { useValidTab } from '../hooks/useValidTab';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

const GoalsTreeView = lazy(() => import('../components/goals/GoalsTreeView'));

// Exported for the nav-manifest tab-coverage guard (server/lib/navManifest.test.js).
export const TABS = [
{ id: 'list', label: 'List', icon: List },
{ id: 'tree', label: 'Tree', icon: TreePine }
];
// Icon per tab id. The manifest (`tabGroup: 'goals'`) owns id/label/order —
// this page owns only how each tab looks; the short page-local labels
// ("List"/"Tree" vs. the manifest's "Goals"/"Goals Tree") come from the
// manifest's `tabLabel`. Throws at import time on drift.
const TAB_PRESENTATION = {
list: { icon: List },
tree: { icon: TreePine },
};

export const TABS = getPageNavTabs('goals').map((tab) => {
const presentation = TAB_PRESENTATION[tab.id];
if (!presentation) throw new Error(`Goals: no tab presentation for manifest tab "${tab.id}"`);
return { ...tab, ...presentation };
});

export default function Goals() {
// `/goals/list/:goalId` carries no `:tab` segment, so `useValidTab` falls back to
Expand Down
18 changes: 18 additions & 0 deletions client/src/pages/Goals.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, it, expect } from 'vitest';
import { TABS } from './Goals';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

// Goals derives its tab bar from the nav manifest's `tabGroup: 'goals'` (#6365)
// — this pins that TABS stays in sync (id, label, declaration order) and that
// every manifest tab has a presentation entry (icon) in Goals.jsx, which would
// otherwise only surface as a thrown import-time error. The page-local
// "List"/"Tree" labels differ from the manifest's "Goals"/"Goals Tree" via
// the manifest's `tabLabel`.
describe('Goals TABS ↔ nav manifest', () => {
it('derives every tab, in order, from the "goals" tabGroup with a presentation entry', () => {
const manifestTabs = getPageNavTabs('goals');
expect(TABS.map((t) => t.id)).toEqual(manifestTabs.map((t) => t.id));
expect(TABS.map((t) => t.label)).toEqual(['List', 'Tree']);
expect(TABS.every((t) => typeof t.icon === 'function' || typeof t.icon === 'object')).toBe(true);
});
});
24 changes: 16 additions & 8 deletions client/src/pages/Insights.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,23 @@ import PageHeader from '../components/PageHeader';
import TabPills from '../components/ui/TabPills';
import PageSkeleton from '../components/ui/PageSkeleton';
import { timeAgo } from '../utils/formatters';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

// Exported for the nav-manifest tab-coverage guard (server/lib/navManifest.test.js).
export const TABS = [
{ id: 'overview', label: 'Overview', icon: Lightbulb },
{ id: 'genome-health', label: 'Genome-Health', icon: Dna },
{ id: 'taste-identity', label: 'Taste & Identity', icon: Palette },
{ id: 'cross-domain', label: 'Cross-Domain Patterns', icon: Link2 },
{ id: 'goal-scorecard', label: 'Goal Scorecard', icon: Target }
];
// Icon per tab id. The manifest (`tabGroup: 'insights'`) owns id/label/order —
// this page owns only how each tab looks. Throws at import time on drift.
const TAB_PRESENTATION = {
overview: { icon: Lightbulb },
'genome-health': { icon: Dna },
'taste-identity': { icon: Palette },
'cross-domain': { icon: Link2 },
'goal-scorecard': { icon: Target },
};

export const TABS = getPageNavTabs('insights').map((tab) => {
const presentation = TAB_PRESENTATION[tab.id];
if (!presentation) throw new Error(`Insights: no tab presentation for manifest tab "${tab.id}"`);
return { ...tab, ...presentation };
});

export function OverviewTab() {
const navigate = useNavigate();
Expand Down
12 changes: 11 additions & 1 deletion client/src/pages/Insights.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ import {
refreshInsightThemes,
refreshInsightNarrative,
} from '../services/api';
import { OverviewTab } from './Insights';
import { OverviewTab, TABS } from './Insights';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

describe('Insights TABS ↔ nav manifest', () => {
it('derives every tab, in order, from the "insights" tabGroup with a presentation entry', () => {
const manifestTabs = getPageNavTabs('insights');
expect(TABS.map((t) => t.id)).toEqual(manifestTabs.map((t) => t.id));
expect(TABS.map((t) => t.label)).toEqual(manifestTabs.map((t) => t.label));
expect(TABS.every((t) => typeof t.icon === 'function' || typeof t.icon === 'object')).toBe(true);
});
});

const renderOverview = () => render(
<MemoryRouter>
Expand Down
29 changes: 19 additions & 10 deletions client/src/pages/Messages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,28 @@ import SyncTab from '../components/messages/SyncTab';
import IMessageTab from '../components/messages/IMessageTab';
import SignalTab from '../components/messages/SignalTab';
import ContactsTab from '../components/messages/ContactsTab';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

// Exported for the nav-manifest tab-coverage guard (server/lib/navManifest.test.js).
// Presentation per tab id. The manifest (`tabGroup: 'messages'`) owns
// id/label/order — this page owns how each tab looks and behaves.
// `fullBleed: true` — tab owns internal scroll/height; Messages skips padded overflow wrapper.
// `needsAccounts: true` — tab renders the account list, so it waits for that fetch.
export const TABS = [
{ id: 'inbox', label: 'Inbox', icon: Mail, needsAccounts: true },
{ id: 'drafts', label: 'Drafts', icon: Mail, needsAccounts: true },
{ id: 'imessage', label: 'iMessage', icon: MessageSquare, fullBleed: true },
{ id: 'signal', label: 'Signal', icon: MessageSquare },
{ id: 'contacts', label: 'Contacts', icon: Users },
{ id: 'sync', label: 'Sync', icon: RefreshCw, needsAccounts: true },
{ id: 'config', label: 'Config', icon: Settings, needsAccounts: true },
];
// Throws at import time if the manifest and this map drift.
const TAB_PRESENTATION = {
inbox: { icon: Mail, needsAccounts: true },
drafts: { icon: Mail, needsAccounts: true },
imessage: { icon: MessageSquare, fullBleed: true },
signal: { icon: MessageSquare },
contacts: { icon: Users },
sync: { icon: RefreshCw, needsAccounts: true },
config: { icon: Settings, needsAccounts: true },
};

export const TABS = getPageNavTabs('messages').map((tab) => {
const presentation = TAB_PRESENTATION[tab.id];
if (!presentation) throw new Error(`Messages: no tab presentation for manifest tab "${tab.id}"`);
return { ...tab, ...presentation };
});

const FULL_BLEED_TAB_IDS = new Set(TABS.filter((t) => t.fullBleed).map((t) => t.id));

Expand Down
17 changes: 17 additions & 0 deletions client/src/pages/Messages.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, it, expect } from 'vitest';
import { TABS } from './Messages';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

// Messages derives its tab bar from the nav manifest's `tabGroup: 'messages'`
// (#6365) — this pins that TABS stays in sync (id, label, declaration order)
// and that every manifest tab has a presentation entry (icon, plus the
// `fullBleed`/`needsAccounts` flags) in Messages.jsx, which would otherwise
// only surface as a thrown import-time error.
describe('Messages TABS ↔ nav manifest', () => {
it('derives every tab, in order, from the "messages" tabGroup with a presentation entry', () => {
const manifestTabs = getPageNavTabs('messages');
expect(TABS.map((t) => t.id)).toEqual(manifestTabs.map((t) => t.id));
expect(TABS.map((t) => t.label)).toEqual(manifestTabs.map((t) => t.label));
expect(TABS.every((t) => typeof t.icon === 'function' || typeof t.icon === 'object')).toBe(true);
});
});
27 changes: 18 additions & 9 deletions client/src/pages/Privacy.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,25 @@ import PrivacyBrokersTab from '../components/privacy/PrivacyBrokersTab';
import SubjectSwitcher from '../components/privacy/SubjectSwitcher';
import SubjectsDrawer from '../components/privacy/SubjectsDrawer';
import { SELF_SUBJECT_ID, privacyTabPath } from '../components/privacy/constants';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

// Exported for the nav-manifest tab-coverage guard (server/lib/navManifest.test.js).
// Each id maps to `/privacy/<id>` and needs a NAV_COMMANDS entry.
export const TABS = [
{ id: 'overview', label: 'Overview', icon: LayoutDashboard },
{ id: 'vault', label: 'Vault', icon: KeyRound },
{ id: 'organizations', label: 'Organizations', icon: Building2 },
{ id: 'changes', label: 'Changes', icon: Repeat },
{ id: 'brokers', label: 'Brokers', icon: ShieldOff },
];
// Icon per tab id. The manifest (`tabGroup: 'privacy'`) owns id/label/order —
// this page owns only how each tab looks; the page-local "Overview" label
// (vs. the manifest's "Privacy") comes from the manifest's `tabLabel`. Throws
// at import time on drift.
const TAB_PRESENTATION = {
overview: { icon: LayoutDashboard },
vault: { icon: KeyRound },
organizations: { icon: Building2 },
changes: { icon: Repeat },
brokers: { icon: ShieldOff },
};

export const TABS = getPageNavTabs('privacy').map((tab) => {
const presentation = TAB_PRESENTATION[tab.id];
if (!presentation) throw new Error(`Privacy: no tab presentation for manifest tab "${tab.id}"`);
return { ...tab, ...presentation };
});

export default function Privacy() {
const navigate = useNavigate();
Expand Down
12 changes: 11 additions & 1 deletion client/src/pages/Privacy.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,17 @@ vi.mock('../services/api', () => ({
draftChangeUpdateEmail: vi.fn(),
}));

import Privacy from './Privacy';
import Privacy, { TABS } from './Privacy';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

describe('Privacy TABS ↔ nav manifest', () => {
it('derives every tab, in order, from the "privacy" tabGroup with a presentation entry', () => {
const manifestTabs = getPageNavTabs('privacy');
expect(TABS.map((t) => t.id)).toEqual(manifestTabs.map((t) => t.id));
expect(TABS.map((t) => t.label)).toEqual(manifestTabs.map((t) => t.label));
expect(TABS.every((t) => typeof t.icon === 'function' || typeof t.icon === 'object')).toBe(true);
});
});
import {
revealVaultRecord, getVaultRecords, getPrivacyStatus, getPrivacySubjects,
} from '../services/api';
Expand Down
28 changes: 19 additions & 9 deletions client/src/pages/Wiki.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@ import WikiBrowseTab from '../components/wiki/tabs/BrowseTab';
import WikiSearchTab from '../components/wiki/tabs/SearchTab';
import WikiGraphTab from '../components/wiki/tabs/GraphTab';
import WikiLogTab from '../components/wiki/tabs/LogTab';

// Exported for the nav-manifest tab-coverage guard (server/lib/navManifest.test.js).
export const TABS = [
{ id: 'overview', label: 'Overview', icon: BarChart3 },
{ id: 'browse', label: 'Browse', icon: FileText },
{ id: 'search', label: 'Search', icon: Search },
{ id: 'graph', label: 'Graph', icon: Network },
{ id: 'log', label: 'Log', icon: Activity }
];
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

// Icon per tab id. The manifest (`tabGroup: 'wiki'`) owns id/label/order —
// this page owns only how each tab looks; the page-local "Overview" label
// (vs. the manifest's "Wiki") comes from the manifest's `tabLabel`. Throws at
// import time on drift.
const TAB_PRESENTATION = {
overview: { icon: BarChart3 },
browse: { icon: FileText },
search: { icon: Search },
graph: { icon: Network },
log: { icon: Activity },
};

export const TABS = getPageNavTabs('wiki').map((tab) => {
const presentation = TAB_PRESENTATION[tab.id];
if (!presentation) throw new Error(`Wiki: no tab presentation for manifest tab "${tab.id}"`);
return { ...tab, ...presentation };
});

export default function Wiki() {
const { tab } = useParams();
Expand Down
12 changes: 11 additions & 1 deletion client/src/pages/Wiki.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,18 @@ vi.mock('../components/wiki/tabs/SearchTab', () => ({ default: () => <div>search
vi.mock('../components/wiki/tabs/GraphTab', () => ({ default: () => <div>graph</div> }));
vi.mock('../components/wiki/tabs/LogTab', () => ({ default: () => <div>log</div> }));

import Wiki from './Wiki';
import Wiki, { TABS } from './Wiki';
import { getNotesVaults, scanNotesVault } from '../services/api';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';

describe('Wiki TABS ↔ nav manifest', () => {
it('derives every tab, in order, from the "wiki" tabGroup with a presentation entry', () => {
const manifestTabs = getPageNavTabs('wiki');
expect(TABS.map((t) => t.id)).toEqual(manifestTabs.map((t) => t.id));
expect(TABS.map((t) => t.label)).toEqual(manifestTabs.map((t) => t.label));
expect(TABS.every((t) => typeof t.icon === 'function' || typeof t.icon === 'object')).toBe(true);
});
});

function LocationProbe() {
const loc = useLocation();
Expand Down
Loading