Skip to content

Commit ed27b0a

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(app-shell): ADR-0048 (A) — emit package-id app URLs from nav (emission layer) (#1696)
Nav now generates `/apps/<packageId>` links instead of `/apps/<name>`: app switching (AppSwitcher/AppSidebar/CommandPalette onAppChange), sidebar base paths (AppSidebar/UnifiedSidebar), create/edit-app, and the AppHeader hidden-app switch all build via `appRouteSegment(app)` (= package id, name fallback). Route-param propagation then carries the package id through every in-app link. Browser-verified (live split backend): entering Studio via a package-id URL, all 53 in-app links emit `/apps/com.objectstack.studio/...` (0 name-based); Studio renders with full sidebar+header; `/apps/studio` (name) still resolves. Typecheck clean. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0c5b468 commit ed27b0a

5 files changed

Lines changed: 15 additions & 15 deletions

File tree

packages/app-shell/src/chrome/CommandPalette.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { useRecordSearch } from '@object-ui/react';
3232
import { useTheme } from './ThemeProvider';
3333
import { useExpressionContext, evaluateVisibility } from '../providers/ExpressionProvider';
3434
import { useObjectTranslation } from '@object-ui/i18n';
35-
import { resolveI18nLabel, getRecordDisplayName } from '../utils';
35+
import { resolveI18nLabel, getRecordDisplayName, appRouteSegment } from '../utils';
3636
import { getIcon } from '../utils/getIcon';
3737
import { useRecentItems } from '../context/RecentItemsProvider';
3838
import { resolveHref } from '@object-ui/layout';
@@ -76,7 +76,7 @@ export function CommandPalette({ apps, activeApp, objects, onAppChange, dataSour
7676
if (!open) setInputValue('');
7777
}, [open]);
7878

79-
const baseUrl = `/apps/${appName || activeApp?.name}`;
79+
const baseUrl = `/apps/${appName || appRouteSegment(activeApp)}`;
8080
const { user } = useAuth();
8181
const templateContext = useMemo(() => ({ currentUserId: user?.id ?? null }), [user?.id]);
8282

@@ -284,7 +284,7 @@ export function CommandPalette({ apps, activeApp, objects, onAppChange, dataSour
284284
<CommandItem
285285
key={app.name}
286286
value={`app ${resolveI18nLabel(app.label, t)} ${app.name}`}
287-
onSelect={() => runCommand(() => onAppChange(app.name))}
287+
onSelect={() => runCommand(() => onAppChange(appRouteSegment(app) ?? app.name))}
288288
>
289289
<Icon className="mr-2 h-4 w-4" />
290290
<span>{resolveI18nLabel(app.label, t)}</span>

packages/app-shell/src/layout/AppHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
6161
import type { BreadcrumbItem as BreadcrumbItemType } from '@object-ui/types';
6262
import { useAuth, getUserInitials } from '@object-ui/auth';
6363
import { useMetadata } from '../providers/MetadataProvider';
64-
import { resolveI18nLabel, preferLocal, matchAppBySegment } from '../utils';
64+
import { resolveI18nLabel, preferLocal, matchAppBySegment, appRouteSegment } from '../utils';
6565
import { getIcon } from '../utils/getIcon';
6666
import { useMobileViewSwitcher } from './MobileViewSwitcherContext';
6767
import { useNavigationContext } from '../context/NavigationContext';
@@ -856,7 +856,7 @@ export function AppHeader({
856856
return (
857857
<DropdownMenuItem
858858
key={`hidden_app_${app.name}`}
859-
onClick={() => navigate(`/apps/${app.name}`)}
859+
onClick={() => navigate(`/apps/${appRouteSegment(app) ?? app.name}`)}
860860
className="cursor-pointer"
861861
>
862862
<AppIcon className="mr-2 h-4 w-4" />

packages/app-shell/src/layout/AppSidebar.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ import { usePermissions } from '@object-ui/permissions';
6161
import { useRecentItems } from '../hooks/useRecentItems';
6262
import { useFavorites } from '../hooks/useFavorites';
6363
import { useNavPins } from '../hooks/useNavPins';
64-
import { resolveI18nLabel, matchAppBySegment } from '../utils';
64+
import { resolveI18nLabel, matchAppBySegment, appRouteSegment } from '../utils';
6565
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
6666
import { useAppContextSelectors } from './ContextSelectors';
6767

@@ -271,7 +271,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
271271
[registeredObjectNames],
272272
);
273273

274-
const basePath = activeApp ? `/apps/${activeAppName}` : '';
274+
const basePath = activeApp ? `/apps/${appRouteSegment(activeApp) ?? activeAppName}` : '';
275275

276276
// Fallback system navigation when no active app exists — routes into the Setup app.
277277
// The marketplace entry is hidden from non-admin members (install is gated to
@@ -341,7 +341,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
341341
{activeApps.map((app: any) => (
342342
<DropdownMenuItem
343343
key={app.name}
344-
onClick={() => onAppChange(app.name)}
344+
onClick={() => onAppChange(appRouteSegment(app) ?? app.name)}
345345
className="gap-2 p-2"
346346
>
347347
<div className="flex size-6 items-center justify-center rounded-sm border">
@@ -359,13 +359,13 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
359359
<div className="font-medium text-muted-foreground">{t('layout.appSwitcher.home')}</div>
360360
</DropdownMenuItem>
361361
<DropdownMenuSeparator />
362-
<DropdownMenuItem className="gap-2 p-2" onClick={() => navigate(`/apps/${activeAppName}/create-app`)} data-testid="add-app-btn">
362+
<DropdownMenuItem className="gap-2 p-2" onClick={() => navigate(`/apps/${appRouteSegment(activeApp) ?? activeAppName}/create-app`)} data-testid="add-app-btn">
363363
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
364364
<Plus className="size-4" />
365365
</div>
366366
<div className="font-medium text-muted-foreground">{t('layout.appSwitcher.addApp')}</div>
367367
</DropdownMenuItem>
368-
<DropdownMenuItem className="gap-2 p-2" onClick={() => navigate(`/apps/${activeAppName}/edit-app/${activeAppName}`)} data-testid="edit-app-btn">
368+
<DropdownMenuItem className="gap-2 p-2" onClick={() => navigate(`/apps/${appRouteSegment(activeApp) ?? activeAppName}/edit-app/${activeAppName}`)} data-testid="edit-app-btn">
369369
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
370370
<Pencil className="size-4" />
371371
</div>
@@ -655,7 +655,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
655655
}
656656
return leaves.slice(0, 5).map((item: any) => {
657657
const NavIcon = getIcon(item.icon);
658-
const baseUrl = activeApp ? `/apps/${activeAppName}` : '';
658+
const baseUrl = activeApp ? `/apps/${appRouteSegment(activeApp) ?? activeAppName}` : '';
659659
const { href } = resolveHref(item, baseUrl, { currentUserId: user?.id ?? null });
660660
return (
661661
<Link key={item.id} to={href} className="flex flex-col items-center gap-0.5 px-2 py-1.5 text-muted-foreground hover:text-foreground transition-colors min-w-[44px] min-h-[44px] justify-center">

packages/app-shell/src/layout/AppSwitcher.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '@object-ui/components';
1818
import { ChevronDown, Check } from 'lucide-react';
1919
import { useMetadata } from '../providers/MetadataProvider';
20-
import { resolveI18nLabel, matchAppBySegment } from '../utils';
20+
import { resolveI18nLabel, matchAppBySegment, appRouteSegment } from '../utils';
2121
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
2222
import { getIcon } from '../utils/getIcon';
2323

@@ -63,7 +63,7 @@ export function AppSwitcher({ activeAppName, onAppChange }: AppSwitcherProps) {
6363
return (
6464
<DropdownMenuItem
6565
key={app.name}
66-
onClick={() => onAppChange(app.name)}
66+
onClick={() => onAppChange(appRouteSegment(app) ?? app.name)}
6767
className="flex items-center gap-2.5 py-2"
6868
>
6969
<div className="flex size-5 shrink-0 items-center justify-center rounded border bg-muted text-muted-foreground">

packages/app-shell/src/layout/UnifiedSidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import { useAuth, useIsWorkspaceAdmin } from '@object-ui/auth';
4747
import { useRecentItems } from '../hooks/useRecentItems';
4848
import { useFavorites } from '../hooks/useFavorites';
4949
import { useNavPins } from '../hooks/useNavPins';
50-
import { resolveI18nLabel, matchAppBySegment } from '../utils';
50+
import { resolveI18nLabel, matchAppBySegment, appRouteSegment } from '../utils';
5151
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
5252
// useObjectLabel provides appLabel/appDescription for convention-based
5353
// i18n lookup — `{ns}.apps.{name}.label` resolves to the translated label
@@ -255,7 +255,7 @@ export function UnifiedSidebar({ activeAppName }: UnifiedSidebarProps) {
255255

256256
// Determine which navigation to show based on context
257257
const navigationItems = context === 'home' ? homeNavigation : appNavigation;
258-
const basePath = context === 'app' && activeApp ? `/apps/${activeApp.name}` : '';
258+
const basePath = context === 'app' && activeApp ? `/apps/${appRouteSegment(activeApp)}` : '';
259259
const isStudioApp = context === 'app' && activeApp?.name === 'studio';
260260
const studioHomeSearch = React.useMemo(() => {
261261
if (!isStudioApp) return '';

0 commit comments

Comments
 (0)