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
73 changes: 11 additions & 62 deletions apps/studio/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import {
Database,
Package,
LayoutDashboard,
ChevronsUpDown,
Sparkles,
Search,
Check,
Zap,
BarChart3,
FileText,
Expand All @@ -33,6 +30,7 @@ import {
ChevronRight,
Settings,
Wrench,
Sparkles,
type LucideIcon,
} from "lucide-react"
import { useState, useEffect, useCallback, useMemo } from "react"
Expand All @@ -43,33 +41,26 @@ import type { InstalledPackage } from '@objectstack/spec/kernel';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarHeader,
SidebarSeparator,
SidebarInput,
SidebarMenuSub,
SidebarMenuSubItem,
SidebarMenuSubButton,
SidebarTrigger,
} from "@/components/ui/sidebar"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

// ─── Icon & label hints ──────────────────────────────────────────────
const META_TYPE_HINTS: Record<string, { label: string; icon: LucideIcon }> = {
Expand Down Expand Up @@ -306,59 +297,10 @@ export function AppSidebar({
return { ...group, visibleTypes, totalItems };
}).filter(g => g.totalItems > 0);

// Package switcher state
const SelectedPkgIcon = selectedPackage ? (PKG_TYPE_ICONS[selectedPackage.manifest?.type] || Package) : Sparkles;
// Package switcher state (no longer used in AppSidebar, moved to TopBar)

Comment on lines +300 to 301
return (
<Sidebar {...props}>
{/* ── Package Switcher ── */}
<SidebarHeader className="border-b">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left hover:bg-sidebar-accent transition-colors">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<SelectedPkgIcon className="h-4 w-4" />
</div>
<div className="flex flex-1 min-w-0 flex-col gap-0.5 leading-none overflow-hidden">
<span className="truncate font-semibold text-sm">
{selectedPackage ? (selectedPackage.manifest?.name || selectedPackage.manifest?.id) : 'ObjectStack'}
</span>
<span className="truncate text-xs text-muted-foreground">
{selectedPackage ? `v${selectedPackage.manifest?.version} · ${selectedPackage.manifest?.type}` : 'Loading packages...'}
</span>
</div>
<ChevronsUpDown className="ml-auto h-4 w-4 shrink-0 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[--radix-dropdown-menu-trigger-width] min-w-64" align="start" sideOffset={4}>
<DropdownMenuLabel>Installed Packages</DropdownMenuLabel>
<DropdownMenuSeparator />
{packages.map((pkg) => {
const Icon = PKG_TYPE_ICONS[pkg.manifest?.type] || Package;
const isSelected = selectedPackage?.manifest?.id === pkg.manifest?.id;
return (
<DropdownMenuItem key={pkg.manifest?.id} onClick={() => onSelectPackage(pkg)} className="gap-2 py-2">
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-primary/10 text-primary">
<Icon className="h-3.5 w-3.5" />
</div>
<div className="flex flex-1 flex-col leading-tight">
<span className="text-sm font-medium">{pkg.manifest?.name || pkg.manifest?.id}</span>
<span className="text-xs text-muted-foreground">
v{pkg.manifest?.version} · {pkg.manifest?.type}
{!pkg.enabled && ' · disabled'}
</span>
</div>
{isSelected && <Check className="h-4 w-4 text-primary" />}
</DropdownMenuItem>
);
})}
{packages.length === 0 && (
<div className="px-2 py-4 text-center text-xs text-muted-foreground">No packages installed</div>
)}
</DropdownMenuContent>
</DropdownMenu>
</SidebarHeader>

<SidebarContent>
{/* ── Overview ── */}
<SidebarGroup>
Expand Down Expand Up @@ -607,6 +549,13 @@ export function AppSidebar({
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarTrigger />
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
)
}
131 changes: 13 additions & 118 deletions apps/studio/src/components/global-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@
* (`/environments/:envId/:package/*`), the package-scoped {@link AppSidebar}
* takes over instead. The two sidebars are mutually exclusive and share the
* same `SidebarProvider` in `routes/__root.tsx`.
*
* Organization switching is now handled in the TopBar, so this sidebar only
* focuses on functional navigation.
*/

import { useMemo } from 'react';
import { Link, useLocation } from '@tanstack/react-router';
import {
Building2,
Check,
ChevronsUpDown,
Plus,
Boxes,
Globe,
Package as PackageIcon,
Expand All @@ -36,126 +35,16 @@ import {
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarSeparator,
SidebarTrigger,
} from '@/components/ui/sidebar';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useOrganizations, useSession } from '@/hooks/useSession';
import { toast } from '@/hooks/use-toast';

/** Header: active organization + switcher. */
function OrgHeader() {
const { organizations, loading, reload } = useOrganizations();
const { session, setActiveOrganization } = useSession();
const activeId = session?.activeOrganizationId ?? undefined;
const active = useMemo(
() => organizations.find((o) => o.id === activeId) ?? null,
[organizations, activeId],
);

const handleSelect = async (id: string) => {
if (id === activeId) return;
try {
await setActiveOrganization(id);
await reload();
toast({ title: 'Organization switched' });
} catch (err) {
toast({
title: 'Failed to switch organization',
description: (err as Error).message,
variant: 'destructive',
});
}
};

return (
<SidebarHeader className="border-b">
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Building2 className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{active?.name ?? (loading ? 'Loading…' : 'Select organization')}
</span>
{active?.slug && (
<span className="truncate text-xs text-muted-foreground">
{active.slug}
</span>
)}
</div>
<ChevronsUpDown className="ml-auto size-4 opacity-60" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56"
align="start"
side="bottom"
sideOffset={4}
>
<DropdownMenuLabel className="text-[10px] uppercase tracking-wider text-muted-foreground">
Organizations
</DropdownMenuLabel>
{organizations.map((org) => (
<DropdownMenuItem
key={org.id}
onSelect={(e) => {
e.preventDefault();
handleSelect(org.id);
}}
className="gap-2"
>
<div className="min-w-0 flex-1">
<div className="truncate font-medium">{org.name}</div>
{org.slug && (
<code className="font-mono text-[11px] text-muted-foreground">
{org.slug}
</code>
)}
</div>
{org.id === activeId && (
<Check className="size-3.5 text-primary" />
)}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link to="/orgs/new" className="gap-2">
<Plus className="size-3.5" />
New organization…
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/orgs" className="gap-2 text-muted-foreground">
Manage organizations
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
);
}
import { useSession } from '@/hooks/useSession';

/**
* Extract the `:envId` segment from the current pathname when the user is
Expand Down Expand Up @@ -184,7 +73,6 @@ export function GlobalSidebar() {

return (
<Sidebar collapsible="icon">
<OrgHeader />
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
Expand Down Expand Up @@ -271,6 +159,13 @@ export function GlobalSidebar() {
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarTrigger />
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
);
}
Loading
Loading