Add a source list to the sidebar and lead with the Records tab
The sidebar now lists every source under the Sources item when it is expanded, so entering a source takes one click instead of two. Bank feeds sort first, then CSV sources, alphabetical within each group, and a small icon marks which is which — the same config.simplefin test the Import page uses. Selecting a source lands on Records rather than Setup: the index route under /sources/:name redirects there and Setup moved to an explicit /setup path at the end of the tab strip. Setup is the rare job; Records is the frequent one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0154qBiPua1PXeet69XmmvQb
This commit is contained in:
parent
3297acf9db
commit
a1e6edc74d
@ -141,6 +141,7 @@ export default function App() {
|
||||
setExpanded={setSidebarExpanded}
|
||||
loginUser={loginUser}
|
||||
onLogout={handleLogout}
|
||||
sources={sources}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -201,7 +202,8 @@ export default function App() {
|
||||
|
||||
<Route path="/sources" element={<SourceList sources={sources} setSources={setSources} setSource={setSource} />} />
|
||||
<Route path="/sources/:name" element={<SourceTabs sources={sources} />}>
|
||||
<Route index element={<SourceDetail sources={sources} setSources={setSources} />} />
|
||||
<Route index element={<Navigate to="records" replace />} />
|
||||
<Route path="setup" element={<SourceDetail sources={sources} setSources={setSources} />} />
|
||||
<Route path="import" element={<ScopedToSource component={Import} />} />
|
||||
<Route path="rules" element={<ScopedToSource component={Rules} onStale={markSourceStale} />} />
|
||||
<Route path="mappings" element={<ScopedToSource component={Mappings} onNeedsReprocess={markNeedsReprocess} />} />
|
||||
|
||||
@ -1,10 +1,37 @@
|
||||
import { Fragment, useMemo } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import useTheme from '../theme.jsx'
|
||||
import { NAV } from './navItems.jsx'
|
||||
|
||||
export default function Sidebar({ expanded, setExpanded, loginUser, onLogout }) {
|
||||
// Same distinction the Import page makes: a source is either on a bank feed or
|
||||
// it gets CSVs uploaded to it.
|
||||
const feedIcon = (
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
|
||||
<path d="M4 10.5a4.5 4.5 0 0 1 4.5 4.5"/>
|
||||
<path d="M4 6a9 9 0 0 1 9 9"/>
|
||||
<circle cx="4.2" cy="14.8" r="1.2" fill="currentColor" stroke="none"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const csvIcon = (
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M4 1.5h5l3 3v10H4z"/>
|
||||
<polyline points="9,1.5 9,4.5 12,4.5"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function Sidebar({ expanded, setExpanded, loginUser, onLogout, sources = [] }) {
|
||||
const { dark, setDark } = useTheme()
|
||||
|
||||
// Bank feeds first, then CSV sources, alphabetical within each group
|
||||
const navSources = useMemo(() => (
|
||||
sources
|
||||
.map(s => ({ ...s, isFeed: !!s.config?.simplefin?.account_id }))
|
||||
.sort((a, b) =>
|
||||
(b.isFeed - a.isFeed) || a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
|
||||
)
|
||||
), [sources])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-surface border-r border-line flex flex-col shrink-0 overflow-hidden transition-all duration-150"
|
||||
@ -32,28 +59,52 @@ export default function Sidebar({ expanded, setExpanded, loginUser, onLogout })
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex flex-col gap-0.5 p-2 flex-1">
|
||||
<nav className="flex flex-col gap-0.5 p-2 flex-1 overflow-y-auto">
|
||||
{NAV.map(({ to, label, icon }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
title={!expanded ? label : undefined}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-2 py-2 rounded w-full transition-colors ${
|
||||
isActive
|
||||
? 'bg-accent-soft text-accent'
|
||||
: 'text-muted hover:bg-raised hover:text-ink'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<span className="shrink-0">{icon}</span>
|
||||
<span
|
||||
className="text-sm whitespace-nowrap transition-opacity duration-100"
|
||||
style={{ opacity: expanded ? 1 : 0, pointerEvents: expanded ? 'auto' : 'none', width: expanded ? 'auto' : 0, overflow: 'hidden' }}
|
||||
<Fragment key={to}>
|
||||
<NavLink
|
||||
to={to}
|
||||
end={to === '/sources'}
|
||||
title={!expanded ? label : undefined}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-2 py-2 rounded w-full transition-colors ${
|
||||
isActive
|
||||
? 'bg-accent-soft text-accent'
|
||||
: 'text-muted hover:bg-raised hover:text-ink'
|
||||
}`
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</NavLink>
|
||||
<span className="shrink-0">{icon}</span>
|
||||
<span
|
||||
className="text-sm whitespace-nowrap transition-opacity duration-100"
|
||||
style={{ opacity: expanded ? 1 : 0, pointerEvents: expanded ? 'auto' : 'none', width: expanded ? 'auto' : 0, overflow: 'hidden' }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</NavLink>
|
||||
|
||||
{/* Jump straight to a source. Collapsed there is no room for names,
|
||||
so the shortcut list only exists when the sidebar is open. */}
|
||||
{to === '/sources' && expanded && navSources.map(({ isFeed, ...s }) => {
|
||||
return (
|
||||
<NavLink
|
||||
key={s.name}
|
||||
to={`/sources/${encodeURIComponent(s.name)}`}
|
||||
title={`${s.name} — ${isFeed ? 'bank feed' : 'CSV'}`}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-2 ml-4 pl-2 pr-2 py-1 rounded border-l border-line-soft transition-colors ${
|
||||
isActive
|
||||
? 'bg-accent-soft text-accent'
|
||||
: 'text-muted hover:bg-raised hover:text-ink'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<span className="shrink-0 opacity-70">{isFeed ? feedIcon : csvIcon}</span>
|
||||
<span className="text-xs truncate">{s.name}</span>
|
||||
</NavLink>
|
||||
)
|
||||
})}
|
||||
</Fragment>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
|
||||
@ -2,13 +2,15 @@ import { NavLink, Outlet, useParams, Link } from 'react-router-dom'
|
||||
|
||||
// Everything scoped to one source lives under /sources/:name, so the source is
|
||||
// in the URL rather than in a global selector.
|
||||
// Records is the tab you want nine times out of ten, so it leads and is what
|
||||
// /sources/:name redirects to; Setup is the rare one and sits at the end.
|
||||
const TABS = [
|
||||
{ to: '', label: 'Setup', end: true },
|
||||
{ to: 'records', label: 'Records' },
|
||||
{ to: 'import', label: 'Import' },
|
||||
{ to: 'rules', label: 'Rules' },
|
||||
{ to: 'mappings', label: 'Mappings' },
|
||||
{ to: 'records', label: 'Records' },
|
||||
{ to: 'pivot', label: 'Pivot' },
|
||||
{ to: 'setup', label: 'Setup' },
|
||||
]
|
||||
|
||||
export default function SourceTabs({ sources }) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user