From 415d21d07100fbf767255cecde6e5c48752daa35 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Thu, 7 May 2026 03:54:32 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(layout):=20rename?= =?UTF-8?q?=20workspace=20switcher=20to=20system=20brand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the layout branding component to reflect that it displays the system identity rather than switching workspaces. Update header usage and layout exports, and remove the now-unused workspace data dependency. --- .../layout/components/app-header.tsx | 9 +- .../layout/components/system-brand.tsx | 84 ++++++ .../layout/components/workspace-switcher.tsx | 276 ------------------ web/default/src/components/layout/index.ts | 2 +- web/default/src/i18n/locales/en.json | 1 + web/default/src/i18n/locales/fr.json | 1 + web/default/src/i18n/locales/ja.json | 1 + web/default/src/i18n/locales/ru.json | 1 + web/default/src/i18n/locales/vi.json | 1 + web/default/src/i18n/locales/zh.json | 1 + 10 files changed, 93 insertions(+), 284 deletions(-) create mode 100644 web/default/src/components/layout/components/system-brand.tsx delete mode 100644 web/default/src/components/layout/components/workspace-switcher.tsx diff --git a/web/default/src/components/layout/components/app-header.tsx b/web/default/src/components/layout/components/app-header.tsx index b5889f50..f540bcbc 100644 --- a/web/default/src/components/layout/components/app-header.tsx +++ b/web/default/src/components/layout/components/app-header.tsx @@ -1,5 +1,4 @@ import { useNotifications } from '@/hooks/use-notifications' -import { useSidebarData } from '@/hooks/use-sidebar-data' import { useTopNavLinks } from '@/hooks/use-top-nav-links' import { ConfigDrawer } from '@/components/config-drawer' import { LanguageSwitcher } from '@/components/language-switcher' @@ -10,8 +9,8 @@ import { Search } from '@/components/search' import { defaultTopNavLinks } from '../config/top-nav.config' import { type TopNavLink } from '../types' import { Header } from './header' +import { SystemBrand } from './system-brand' import { TopNav } from './top-nav' -import { WorkspaceSwitcher } from './workspace-switcher' /** * General application Header component @@ -89,7 +88,6 @@ export function AppHeader({ // Prioritize dynamically generated links from backend const dynamicLinks = useTopNavLinks() const links = dynamicLinks.length > 0 ? dynamicLinks : navLinks - const sidebarData = useSidebarData() // Notifications hook const notifications = useNotifications() @@ -97,10 +95,7 @@ export function AppHeader({ return ( <>
- + {leftContent ? (
{leftContent}
diff --git a/web/default/src/components/layout/components/system-brand.tsx b/web/default/src/components/layout/components/system-brand.tsx new file mode 100644 index 00000000..51e59ae1 --- /dev/null +++ b/web/default/src/components/layout/components/system-brand.tsx @@ -0,0 +1,84 @@ +import { Link } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' +import { cn } from '@/lib/utils' +import { useStatus } from '@/hooks/use-status' +import { useSystemConfig } from '@/hooks/use-system-config' +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from '@/components/ui/sidebar' + +type SystemBrandProps = { + defaultName?: string + defaultVersion?: string + /** + * Visual layout: + * - 'sidebar': stacked card style (used inside the sidebar header). + * - 'inline': compact horizontal pill (used inside the top app bar). + */ + variant?: 'sidebar' | 'inline' +} + +/** + * System brand component + * Displays current system logo + name. + * - inline: compact pill in the top app bar; clicking navigates to home (/) + * - sidebar: stacked card in the sidebar header (display only) + */ +export function SystemBrand(props: SystemBrandProps) { + const { t } = useTranslation() + const { status } = useStatus() + const { logo } = useSystemConfig() + + const variant = props.variant ?? 'sidebar' + const name = status?.system_name || props.defaultName || 'New API' + const version = + status?.version || props.defaultVersion || t('Unknown version') + + if (variant === 'inline') { + return ( + +
+ {t('Logo')} +
+ {name} + + ) + } + + return ( + + + } + > +
+ {t('Logo')} +
+
+ {name} + {version} +
+
+
+
+ ) +} diff --git a/web/default/src/components/layout/components/workspace-switcher.tsx b/web/default/src/components/layout/components/workspace-switcher.tsx deleted file mode 100644 index f7252afe..00000000 --- a/web/default/src/components/layout/components/workspace-switcher.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import * as React from 'react' -import { useNavigate, useLocation } from '@tanstack/react-router' -import { ChevronsUpDown } from 'lucide-react' -import { useTranslation } from 'react-i18next' -import { useAuthStore } from '@/stores/auth-store' -import { ROLE } from '@/lib/roles' -import { cn } from '@/lib/utils' -import { useStatus } from '@/hooks/use-status' -import { useSystemConfig } from '@/hooks/use-system-config' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from '@/components/ui/sidebar' -import { useWorkspace } from '../context/workspace-context' -import { getWorkspaceByPath, WORKSPACE_IDS } from '../lib/workspace-registry' -import { type Workspace } from '../types' - -type WorkspaceSwitcherProps = { - workspaces: Workspace[] - defaultName?: string - defaultVersion?: string - /** - * Visual layout: - * - 'sidebar': stacked card style (used inside the sidebar header). - * - 'inline': compact horizontal pill (used inside the top app bar). - */ - variant?: 'sidebar' | 'inline' -} - -/** - * Workspace switcher component - * Allows users to switch between different workspaces - * - Regular users can only see the default workspace - * - Super administrators can see the system settings workspace - */ -export function WorkspaceSwitcher({ - workspaces, - defaultName = 'New API', - defaultVersion, - variant = 'sidebar', -}: WorkspaceSwitcherProps) { - const { t } = useTranslation() - const navigate = useNavigate() - const { pathname } = useLocation() - const { isMobile } = useSidebar() - const { status } = useStatus() - const { logo } = useSystemConfig() - const isSuperAdmin = useAuthStore( - (state) => state.auth.user?.role === ROLE.SUPER_ADMIN - ) - const { activeWorkspace, setActiveWorkspace } = useWorkspace() - - // Handle workspace list: - // 1. Populate first workspace with system info - // 2. Filter based on user permissions (non-super admins cannot see system settings) - const availableWorkspaces = React.useMemo( - () => - workspaces - .map((workspace, index) => - index === 0 - ? { - ...workspace, - name: status?.system_name || defaultName, - plan: status?.version || defaultVersion || t('Unknown version'), - } - : workspace - ) - .filter( - (workspace) => - isSuperAdmin || workspace.id !== WORKSPACE_IDS.SYSTEM_SETTINGS - ), - [ - workspaces, - status?.system_name, - status?.version, - defaultName, - defaultVersion, - isSuperAdmin, - t, - ] - ) - - // Initialize and synchronize active workspace - // Detect from URL first, then sync from activeWorkspace - React.useEffect(() => { - // Detect which workspace should be active from workspace registry - const detectedWorkspace = getWorkspaceByPath(pathname) - - if (detectedWorkspace.id === WORKSPACE_IDS.SYSTEM_SETTINGS) { - // Currently in system settings route, should activate System Settings workspace - const systemSettingsWorkspace = availableWorkspaces.find( - (w) => w.id === WORKSPACE_IDS.SYSTEM_SETTINGS - ) - if (systemSettingsWorkspace) { - setActiveWorkspace(systemSettingsWorkspace) - } - } else { - // Currently in main workspace route, should activate main workspace - const mainWorkspace = - availableWorkspaces.find((w) => w.id === WORKSPACE_IDS.DEFAULT) || - availableWorkspaces[0] - if (mainWorkspace) { - setActiveWorkspace(mainWorkspace) - } - } - }, [pathname, availableWorkspaces, setActiveWorkspace]) - - const handleWorkspaceChange = (workspace: Workspace) => { - // Only navigate, let useEffect synchronize workspace state based on new pathname - // This avoids race conditions and context loss issues - if (workspace.id === WORKSPACE_IDS.SYSTEM_SETTINGS) { - navigate({ to: '/system-settings/site' }) - } else { - navigate({ to: '/dashboard' }) - } - } - - if (!activeWorkspace) { - return null - } - - const canSwitchWorkspace = availableWorkspaces.length > 1 - - const renderWorkspaceList = () => ( - - - {t('Workspaces')} - - {availableWorkspaces.map((workspace, index) => ( - handleWorkspaceChange(workspace)} - className='gap-2 p-2' - > - {index === 0 ? ( -
- Logo -
- ) : ( -
- -
- )} - {workspace.name} -
- ))} -
- ) - - if (variant === 'inline') { - const inlineLogo = - activeWorkspace.id === WORKSPACE_IDS.SYSTEM_SETTINGS ? ( -
- -
- ) : ( -
- {t('Logo')} -
- ) - - const inlineButtonClass = cn( - 'inline-flex h-7 items-center gap-1.5 rounded-md px-1.5 text-sm font-medium text-foreground outline-none select-none transition-colors', - 'hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring/40', - 'data-popup-open:bg-accent' - ) - - if (!canSwitchWorkspace) { - return ( -
- {inlineLogo} - {activeWorkspace.name} -
- ) - } - - return ( - - - {inlineLogo} - {activeWorkspace.name} - - - - {renderWorkspaceList()} - - - ) - } - - const workspaceButtonContent = ( - <> - {activeWorkspace.id === WORKSPACE_IDS.SYSTEM_SETTINGS ? ( -
- -
- ) : ( -
- {t('Logo')} -
- )} -
- {activeWorkspace.name} - {activeWorkspace.plan} -
- {canSwitchWorkspace && ( - - )} - - ) - - return ( - - - {canSwitchWorkspace ? ( - - - } - > - {workspaceButtonContent} - - - {renderWorkspaceList()} - - - ) : ( - } - > - {workspaceButtonContent} - - )} - - - ) -} diff --git a/web/default/src/components/layout/index.ts b/web/default/src/components/layout/index.ts index 131580f4..2b21a32e 100644 --- a/web/default/src/components/layout/index.ts +++ b/web/default/src/components/layout/index.ts @@ -16,7 +16,7 @@ export { Main } from './components/main' export { PageFooterPortal } from './components/page-footer' export { NavGroup } from './components/nav-group' export { SectionPageLayout } from './components/section-page-layout' -export { WorkspaceSwitcher } from './components/workspace-switcher' +export { SystemBrand } from './components/system-brand' export { TopNav } from './components/top-nav' export { MobileDrawer } from './components/mobile-drawer' diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 475a5791..413d70f8 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -1826,6 +1826,7 @@ "Go back and edit": "Go back and edit", "Go to Dashboard": "Go to Dashboard", "Go to first page": "Go to first page", + "Go to home": "Go to home", "Go to io.net API Keys": "Go to io.net API Keys", "Go to last page": "Go to last page", "Go to next page": "Go to next page", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 73a3202e..c26f7933 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -1826,6 +1826,7 @@ "Go back and edit": "Retour et modifier", "Go to Dashboard": "Aller au tableau de bord", "Go to first page": "Aller à la première page", + "Go to home": "Retour à l'accueil", "Go to io.net API Keys": "Accéder aux clés API io.net", "Go to last page": "Aller à la dernière page", "Go to next page": "Aller à la page suivante", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 9de3b03f..3b7a824b 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -1826,6 +1826,7 @@ "Go back and edit": "戻って編集", "Go to Dashboard": "ダッシュボードへ移動", "Go to first page": "最初のページへ移動", + "Go to home": "ホームへ戻る", "Go to io.net API Keys": "io.net API キーへ移動", "Go to last page": "最後のページへ移動", "Go to next page": "次のページへ移動", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index b6895ff4..e316832b 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -1826,6 +1826,7 @@ "Go back and edit": "Вернуться и изменить", "Go to Dashboard": "Перейти в панель управления", "Go to first page": "Перейти на первую страницу", + "Go to home": "На главную", "Go to io.net API Keys": "Перейти к ключам API io.net", "Go to last page": "Перейти на последнюю страницу", "Go to next page": "Перейти на следующую страницу", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index cf2040d1..15fadb5f 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -1826,6 +1826,7 @@ "Go back and edit": "Quay lại và chỉnh sửa", "Go to Dashboard": "Truy cập Dashboard", "Go to first page": "Go to the first page", + "Go to home": "Về trang chủ", "Go to io.net API Keys": "Đi đến Khóa API io.net", "Go to last page": "Go to the last page", "Go to next page": "Đi đến trang tiếp theo", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index f9d11713..e9fa9844 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -1826,6 +1826,7 @@ "Go back and edit": "返回修改", "Go to Dashboard": "前往仪表板", "Go to first page": "前往首页", + "Go to home": "返回主页", "Go to io.net API Keys": "前往 io.net API 密钥", "Go to last page": "前往末页", "Go to next page": "前往下一页",