Skip to content

Commit be252ae

Browse files
committed
feat(top-bar): enhance package switching functionality and update imports
1 parent fc3bc4a commit be252ae

3 files changed

Lines changed: 57 additions & 19 deletions

File tree

apps/studio/src/components/top-bar.tsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
* - Right segment: Global search placeholder, mode badge, ThemeToggle, UserMenu
1111
*/
1212

13-
import { Link, useLocation, useParams } from '@tanstack/react-router';
14-
import { useMemo } from 'react';
13+
import { Link, useLocation, useNavigate, useParams } from '@tanstack/react-router';
14+
import { useCallback, useMemo } from 'react';
15+
import type { InstalledPackage } from '@objectstack/spec/kernel';
1516
import { Separator } from '@/components/ui/separator';
1617
import {
1718
Breadcrumb,
@@ -28,9 +29,11 @@ import { Boxes, Cpu, Search } from 'lucide-react';
2829
import { config } from '@/lib/config';
2930
import { ProjectSwitcher } from '@/components/project-switcher';
3031
import { OrganizationSwitcher } from '@/components/organization-switcher';
32+
import { PackageSwitcher } from '@/components/package-switcher';
3133
import { UserMenu } from '@/components/user-menu';
3234
import { SidebarTrigger } from '@/components/ui/sidebar';
3335
import { useActiveOrganizationId } from '@/hooks/useSession';
36+
import { useEnvAwarePackages } from '@/hooks/useProjectAwarePackages';
3437

3538
const META_TYPE_LABELS: Record<string, string> = {
3639
action: 'Actions',
@@ -75,6 +78,7 @@ function SlashDivider() {
7578

7679
export function TopBar() {
7780
const location = useLocation();
81+
const navigate = useNavigate();
7882
const activeOrgId = useActiveOrganizationId();
7983
const params = useParams({ strict: false }) as {
8084
package?: string;
@@ -84,6 +88,38 @@ export function TopBar() {
8488
orgId?: string;
8589
};
8690

91+
// Load packages installed in the current project so users can switch
92+
// between them from the top-bar (e.g. while viewing metadata).
93+
const { packages } = useEnvAwarePackages(params.projectId);
94+
95+
// Resolve the current package from the URL segment. Match either the
96+
// full reverse-domain id (e.g. com.example.crm) or the last segment (crm).
97+
const selectedPackage = useMemo<InstalledPackage | null>(() => {
98+
if (!params.package || !packages.length) return null;
99+
return (
100+
packages.find(
101+
(p) =>
102+
p.manifest?.id === params.package ||
103+
p.manifest?.id?.split('.').pop() === params.package,
104+
) ?? null
105+
);
106+
}, [packages, params.package]);
107+
108+
const handleSelectPackage = useCallback(
109+
(pkg: InstalledPackage) => {
110+
const nextId = pkg.manifest?.id;
111+
if (!nextId || !params.projectId) return;
112+
// Switching package invalidates the current metadata path (the same
113+
// type/name may not exist in the target package), so land on the
114+
// package overview.
115+
navigate({
116+
to: '/projects/$projectId/$package',
117+
params: { projectId: params.projectId, package: nextId },
118+
});
119+
},
120+
[navigate, params.projectId],
121+
);
122+
87123
// Infer view type from pathname
88124
const viewType = useMemo(() => {
89125
const pathname = location.pathname;
@@ -182,6 +218,16 @@ export function TopBar() {
182218
{!config.singleProject && <OrganizationSwitcher />}
183219
{(!config.singleProject && activeOrgId) && <SlashDivider />}
184220
{!config.singleProject && <ProjectSwitcher />}
221+
{params.projectId && params.package && (
222+
<>
223+
<SlashDivider />
224+
<PackageSwitcher
225+
packages={packages}
226+
selectedPackage={selectedPackage}
227+
onSelectPackage={handleSelectPackage}
228+
/>
229+
</>
230+
)}
185231
</div>
186232
{/* Mobile: Show only current page breadcrumb */}
187233
<div className="sm:hidden min-w-0 flex-1">

apps/studio/src/hooks/usePackages.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,15 @@ import { useClient } from '@objectstack/client-react';
55
import type { InstalledPackage } from '@objectstack/spec/kernel';
66

77
export interface UsePackagesOptions {
8-
/**
9-
* When `'project'` (default), exclude platform-scoped packages (they are
10-
* runtime-global, not env-installable). When `'platform'`, return ONLY
11-
* platform-scoped packages — used by the platform pseudo-project surface.
12-
*/
13-
scope?: 'project' | 'platform';
8+
// Reserved for future filtering options. Scope-based filtering has been
9+
// intentionally removed — every installed package (system + project) is
10+
// visible from every surface so the user can always switch between them.
1411
}
1512

1613
/**
1714
* Hook to fetch and manage installed packages
1815
*/
19-
export function usePackages(options: UsePackagesOptions = {}) {
20-
const { scope = 'project' } = options;
16+
export function usePackages(_options: UsePackagesOptions = {}) {
2117
const client = useClient();
2218
const [packages, setPackages] = useState<InstalledPackage[]>([]);
2319
const [selectedPackage, setSelectedPackage] = useState<InstalledPackage | null>(null);
@@ -30,14 +26,12 @@ export function usePackages(options: UsePackagesOptions = {}) {
3026
try {
3127
const result = await client.packages.list();
3228
const all: InstalledPackage[] = result?.packages || [];
33-
// Always exclude dev-workspace (monorepo aggregator) and unversioned packages.
34-
// Then narrow by scope.
29+
// Exclude dev-workspace (monorepo aggregator) and unversioned packages;
30+
// otherwise return every package regardless of `manifest.scope`.
3531
const items = all.filter((p) => {
3632
if (p.manifest?.version === '0.0.0') return false;
3733
if (p.manifest?.id === 'dev-workspace') return false;
38-
const pkgScope = (p.manifest as any)?.scope;
39-
if (scope === 'platform') return pkgScope === 'platform';
40-
return pkgScope !== 'platform';
34+
return true;
4135
});
4236
console.log('[App] Fetched packages:', items.map((p) => p.manifest?.name || p.manifest?.id));
4337
if (mounted) {
@@ -57,7 +51,7 @@ export function usePackages(options: UsePackagesOptions = {}) {
5751

5852
loadPackages();
5953
return () => { mounted = false; };
60-
}, [client, scope]);
54+
}, [client]);
6155

6256
return { packages, selectedPackage, setSelectedPackage };
6357
}

apps/studio/src/hooks/useProjectAwarePackages.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ import { isPlatformProject } from '@/lib/platform-project';
2020
*/
2121
export function useEnvAwarePackages(envId: string | undefined) {
2222
const isPlatform = isPlatformProject(envId);
23-
const { packages: globalPkgs } = usePackages({
24-
scope: isPlatform ? 'platform' : 'project',
25-
});
23+
const { packages: globalPkgs } = usePackages();
2624
const { packages: installedRecords } = useProjectPackages(envId);
2725

2826
const [selectedPackage, setSelectedPackage] = useState<InstalledPackage | null>(null);

0 commit comments

Comments
 (0)