✨ refactor(web/default): adopt drill-in sidebar pattern for System Settings
Replace the ad-hoc "workspace" abstraction with a focused, URL-driven
"sidebar view" registry that implements the modern Vercel / Cloudflare
drill-in pattern: clicking a top-level entry (e.g. System Settings)
swaps the sidebar to a contextual workspace, with a `← Back to
Dashboard` affordance, instead of stacking sub-navigation in the root.
Architecture
------------
- types.ts
+ SidebarView — declarative nested view config
(id, pathPattern, parent, getNavGroups)
+ SidebarViewParent — back-navigation descriptor
+ ResolvedSidebarView — { key, view, navGroups } returned by hook
+ SidebarData — slimmed to { navGroups } only
- Workspace — removed (logo/plan never rendered)
- lib/sidebar-view-registry.ts (new, replaces workspace-registry.ts)
+ SIDEBAR_VIEWS array — single source of truth for nested views
+ resolveSidebarView(pathname)
+ getNavGroupsForPath(pathname, t) — back-compat helper for the
command palette
- config/system-settings.config.ts
Refactored to export a single SYSTEM_SETTINGS_VIEW (SidebarView)
with parent `/dashboard/overview` + label `Back to Dashboard`.
- components/sidebar-view-header.tsx (new)
Renders only the back affordance (chevron + label). Uses the
default SidebarMenuButton size so its typography matches the
nav items below; collapses gracefully into icon mode via the
existing tooltip behavior. The redundant "title + icon" row was
removed — workspace context is already carried by the nav groups.
- hooks/use-sidebar-view.ts (new)
Encapsulates view resolution and root-nav filtering:
· matched view → returns its nav groups verbatim (route-level
beforeLoad guards already enforce access);
· no match → returns root nav groups, narrowed by user
role (admin gate) and useSidebarConfig
(admin × user sidebar_modules overlay).
- components/app-sidebar.tsx
Now a thin presentation layer: reads { key, view, navGroups }
from useSidebarView() and orchestrates the view transition via
AnimatePresence + MOTION_VARIANTS.sidebarSlide (respects
prefers-reduced-motion). No logic, no role checks, no path
matching — those live in the hook.
- components/command-menu.tsx
Switched to the new getNavGroupsForPath() API; behavior preserved.
Cleanup
-------
- Deleted layout/context/workspace-context.tsx (zero consumers).
- Deleted layout/lib/workspace-registry.ts and its
workspace-registry.example.ts companion (over-abstracted: name/id
metadata, isInWorkspace / getAllWorkspaces / WORKSPACE_IDS were
registered but never read).
- Removed `workspaces` field from useSidebarData (never consumed
after the top-switcher was dropped).
- Dropped WorkspaceProvider from authenticated-layout.tsx.
- Trimmed dead `Manage and configure` translation key from all six
locale files and from static-keys.ts.
i18n
----
Added the `Back to Dashboard` key to en, zh, fr, ja, ru, vi, and
registered it in static-keys.ts under "Sidebar views".
Verification
------------
- bun run typecheck: passes
- Lint: no new warnings/errors on the touched files
- Adding a new drill-in workspace now only requires registering a
SidebarView in SIDEBAR_VIEWS — no changes to AppSidebar required.
This commit is contained in:
+17
-21
@@ -17,39 +17,35 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Activity,
|
||||
Key,
|
||||
FileText,
|
||||
Wallet,
|
||||
Box,
|
||||
Users,
|
||||
CreditCard,
|
||||
FileText,
|
||||
FlaskConical,
|
||||
Key,
|
||||
LayoutDashboard,
|
||||
ListTodo,
|
||||
MessageSquare,
|
||||
Radio,
|
||||
Settings,
|
||||
Ticket,
|
||||
User,
|
||||
Command,
|
||||
Radio,
|
||||
FlaskConical,
|
||||
MessageSquare,
|
||||
CreditCard,
|
||||
ListTodo,
|
||||
Settings,
|
||||
Users,
|
||||
Wallet,
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { WORKSPACE_IDS } from '@/components/layout/lib/workspace-registry'
|
||||
import { type SidebarData } from '@/components/layout/types'
|
||||
|
||||
/**
|
||||
* Root navigation groups for the application sidebar.
|
||||
*
|
||||
* These are shown when the URL does not match any nested sidebar view
|
||||
* registered in `layout/lib/sidebar-view-registry.ts`.
|
||||
*/
|
||||
export function useSidebarData(): SidebarData {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return {
|
||||
workspaces: [
|
||||
{
|
||||
id: WORKSPACE_IDS.DEFAULT,
|
||||
name: '', // Dynamically fetches system name
|
||||
logo: Command,
|
||||
plan: '', // Dynamically fetches system version
|
||||
},
|
||||
],
|
||||
navGroups: [
|
||||
{
|
||||
id: 'chat',
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright (C) 2023-2026 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useMemo } from 'react'
|
||||
import { useLocation } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import { ROLE } from '@/lib/roles'
|
||||
import { resolveSidebarView } from '@/components/layout/lib/sidebar-view-registry'
|
||||
import type { NavGroup, ResolvedSidebarView } from '@/components/layout/types'
|
||||
import { useSidebarConfig } from './use-sidebar-config'
|
||||
import { useSidebarData } from './use-sidebar-data'
|
||||
|
||||
/** Sentinel key used for the root navigation in animation `key=` props */
|
||||
const ROOT_VIEW_KEY = '__root'
|
||||
|
||||
/**
|
||||
* Resolve the active sidebar view for the current location.
|
||||
*
|
||||
* - Returns the matching nested {@link SidebarView} (with its nav
|
||||
* groups) when the URL belongs to a registered drill-in workspace.
|
||||
* - Otherwise returns the root navigation, narrowed by:
|
||||
* · admin-only group visibility (role-based);
|
||||
* · `useSidebarConfig` (admin × user `sidebar_modules` overlay).
|
||||
*
|
||||
* Nested views are intentionally NOT passed through `useSidebarConfig`
|
||||
* — those filters target known dashboard URLs only, and gating is
|
||||
* already enforced at the route level (`beforeLoad` redirects).
|
||||
*/
|
||||
export function useSidebarView(): ResolvedSidebarView {
|
||||
const { t } = useTranslation()
|
||||
const pathname = useLocation({ select: (l) => l.pathname })
|
||||
const userRole = useAuthStore((s) => s.auth.user?.role)
|
||||
const rootSidebarData = useSidebarData()
|
||||
const configFilteredRoot = useSidebarConfig(rootSidebarData.navGroups)
|
||||
|
||||
const rootNavGroups = useMemo<NavGroup[]>(() => {
|
||||
const isAdmin = userRole !== undefined && userRole >= ROLE.ADMIN
|
||||
return configFilteredRoot.filter((group) =>
|
||||
group.id === 'admin' ? isAdmin : true
|
||||
)
|
||||
}, [configFilteredRoot, userRole])
|
||||
|
||||
const view = resolveSidebarView(pathname)
|
||||
|
||||
if (view) {
|
||||
return {
|
||||
key: view.id,
|
||||
view,
|
||||
navGroups: view.getNavGroups(t),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: ROOT_VIEW_KEY,
|
||||
view: null,
|
||||
navGroups: rootNavGroups,
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -19,8 +19,8 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import { useStatus } from '@/hooks/use-status'
|
||||
import { parseHeaderNavModulesFromStatus } from '@/lib/nav-modules'
|
||||
import { useStatus } from '@/hooks/use-status'
|
||||
|
||||
export type TopNavLink = {
|
||||
title: string
|
||||
|
||||
Reference in New Issue
Block a user