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}
|
setExpanded={setSidebarExpanded}
|
||||||
loginUser={loginUser}
|
loginUser={loginUser}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
|
sources={sources}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -201,7 +202,8 @@ export default function App() {
|
|||||||
|
|
||||||
<Route path="/sources" element={<SourceList sources={sources} setSources={setSources} setSource={setSource} />} />
|
<Route path="/sources" element={<SourceList sources={sources} setSources={setSources} setSource={setSource} />} />
|
||||||
<Route path="/sources/:name" element={<SourceTabs sources={sources} />}>
|
<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="import" element={<ScopedToSource component={Import} />} />
|
||||||
<Route path="rules" element={<ScopedToSource component={Rules} onStale={markSourceStale} />} />
|
<Route path="rules" element={<ScopedToSource component={Rules} onStale={markSourceStale} />} />
|
||||||
<Route path="mappings" element={<ScopedToSource component={Mappings} onNeedsReprocess={markNeedsReprocess} />} />
|
<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 { NavLink } from 'react-router-dom'
|
||||||
import useTheme from '../theme.jsx'
|
import useTheme from '../theme.jsx'
|
||||||
import { NAV } from './navItems.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()
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="bg-surface border-r border-line flex flex-col shrink-0 overflow-hidden transition-all duration-150"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Nav */}
|
{/* 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 }) => (
|
{NAV.map(({ to, label, icon }) => (
|
||||||
<NavLink
|
<Fragment key={to}>
|
||||||
key={to}
|
<NavLink
|
||||||
to={to}
|
to={to}
|
||||||
title={!expanded ? label : undefined}
|
end={to === '/sources'}
|
||||||
className={({ isActive }) =>
|
title={!expanded ? label : undefined}
|
||||||
`flex items-center gap-3 px-2 py-2 rounded w-full transition-colors ${
|
className={({ isActive }) =>
|
||||||
isActive
|
`flex items-center gap-3 px-2 py-2 rounded w-full transition-colors ${
|
||||||
? 'bg-accent-soft text-accent'
|
isActive
|
||||||
: 'text-muted hover:bg-raised hover:text-ink'
|
? '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' }}
|
|
||||||
>
|
>
|
||||||
{label}
|
<span className="shrink-0">{icon}</span>
|
||||||
</span>
|
<span
|
||||||
</NavLink>
|
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>
|
</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
|
// Everything scoped to one source lives under /sources/:name, so the source is
|
||||||
// in the URL rather than in a global selector.
|
// 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 = [
|
const TABS = [
|
||||||
{ to: '', label: 'Setup', end: true },
|
{ to: 'records', label: 'Records' },
|
||||||
{ to: 'import', label: 'Import' },
|
{ to: 'import', label: 'Import' },
|
||||||
{ to: 'rules', label: 'Rules' },
|
{ to: 'rules', label: 'Rules' },
|
||||||
{ to: 'mappings', label: 'Mappings' },
|
{ to: 'mappings', label: 'Mappings' },
|
||||||
{ to: 'records', label: 'Records' },
|
|
||||||
{ to: 'pivot', label: 'Pivot' },
|
{ to: 'pivot', label: 'Pivot' },
|
||||||
|
{ to: 'setup', label: 'Setup' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function SourceTabs({ sources }) {
|
export default function SourceTabs({ sources }) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user