Skip to content

Commit c2f7d73

Browse files
authored
Merge pull request #900 from objectstack-ai/copilot/optimize-ui-navigation-table
2 parents a35507e + acf367d commit c2f7d73

File tree

18 files changed

+234
-43
lines changed

18 files changed

+234
-43
lines changed

ROADMAP.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ObjectUI Development Roadmap
22

3-
> **Last Updated:** February 26, 2026
3+
> **Last Updated:** February 27, 2026
44
> **Current Version:** v0.5.x
55
> **Spec Version:** @objectstack/spec v3.0.10
66
> **Client Version:** @objectstack/client v3.0.10
@@ -972,6 +972,38 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
972972
- [x] 411 ListView + ViewTabBar tests passing (255 plugin-list tests including 9 new toolbar/collapse tests)
973973
- [x] 11 AppSidebar tests passing
974974

975+
### P2.9 Platform UI Navigation & Table Optimization ✅
976+
977+
> Platform-level sidebar, navigation, and grid/table UX improvements (Issue #XX).
978+
979+
**Sidebar Navigation:**
980+
- [x] Pin icons show-on-hover: `SidebarMenuAction` in `NavigationRenderer` now uses `showOnHover={!item.pinned}` — unpinned items show pin icon only on hover; pinned items always show unpin icon. Applied to both `action` and leaf navigation item types.
981+
- [x] Search placeholder contrast: Search icon in AppSidebar improved from `opacity-50``opacity-70` for better readability.
982+
- [x] Recent section position: Recent items section moved above Record Favorites in AppSidebar for quicker access to recently visited items.
983+
- [x] Favorites section: Already hides automatically when no pinned items exist (verified).
984+
- [x] Resizable sidebar width: `SidebarRail` enhanced with pointer-event drag-to-resize (min 200px, max 480px). Width persisted to `localStorage`. Click toggles sidebar, double-click resets to default. `useSidebar()` hook now exposes `sidebarWidth` and `setSidebarWidth`.
985+
986+
**Grid/Table Field Inference:**
987+
- [x] Percent field auto-inference: `inferColumnType()` in ObjectGrid now detects fields with names containing `probability`, `percent`, `percentage`, `completion`, `progress`, `rate` and assigns `PercentCellRenderer` with progress bar display.
988+
- [x] ISO datetime fallback: ObjectGrid `inferColumnType()` now detects ISO 8601 datetime strings (`YYYY-MM-DDTHH:MM`) in data values as a catch-all for fields whose names don't match date/datetime patterns.
989+
- [x] Date/datetime human-friendly display: `DateCellRenderer` (relative format) and `DateTimeCellRenderer` (split date/time) already registered in field registry for all grid/table views.
990+
- [x] Currency/status/boolean renderers: Already implemented with proper formatting (currency symbol, Badge colors, checkbox display).
991+
992+
**Header & Breadcrumb i18n:**
993+
- [x] AppHeader breadcrumb labels (`Dashboards`, `Pages`, `Reports`, `System`) now use `t()` translation via `useObjectTranslation`.
994+
- [x] `console.breadcrumb` i18n keys added to all 11 locales (en, zh, ja, ko, de, fr, es, pt, ru, ar).
995+
- [x] `header-bar` renderer: `resolveCrumbLabel()` handles both string and `I18nLabel` objects for breadcrumb labels.
996+
- [x] `breadcrumb` renderer: `resolveItemLabel()` handles both string and `I18nLabel` objects for item labels.
997+
998+
**Tests:**
999+
- [x] 46 NavigationRenderer tests passing (pin/favorites/search/reorder)
1000+
- [x] 75 field cell renderer tests passing (date/datetime/select/boolean/percent)
1001+
- [x] 263 ObjectGrid tests passing (inference, rendering, accessibility)
1002+
- [x] 28 DataTable tests passing
1003+
- [x] 78 Layout tests passing (NavigationRenderer + AppSchemaRenderer)
1004+
- [x] 11 AppSidebar tests passing
1005+
- [x] 32 i18n tests passing
1006+
9751007
### P2.5 PWA & Offline (Real Sync)
9761008

9771009
- [ ] Background sync queue → real server sync (replace simulation)

apps/console/src/components/AppHeader.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { ConnectionStatus } from './ConnectionStatus';
3535
import { ActivityFeed, type ActivityItem } from './ActivityFeed';
3636
import type { ConnectionState } from '../dataSource';
3737
import { useAdapter } from '../context/AdapterProvider';
38+
import { useObjectTranslation } from '@object-ui/i18n';
3839

3940
/** Convert a slug like "crm_dashboard" or "audit-log" to "Crm Dashboard" / "Audit Log" */
4041
function humanizeSlug(slug: string): string {
@@ -55,6 +56,7 @@ export function AppHeader({ appName, objects, connectionState, presenceUsers, ac
5556
const params = useParams();
5657
const { isOnline } = useOffline();
5758
const dataSource = useAdapter();
59+
const { t } = useObjectTranslation();
5860

5961
const [apiPresenceUsers, setApiPresenceUsers] = useState<PresenceUser[] | null>(null);
6062
const [apiActivities, setApiActivities] = useState<ActivityItem[] | null>(null);
@@ -110,22 +112,22 @@ export function AppHeader({ appName, objects, connectionState, presenceUsers, ac
110112
];
111113

112114
if (routeType === 'dashboard') {
113-
breadcrumbItems.push({ label: 'Dashboards', href: `${breadcrumbItems[0].href}` });
115+
breadcrumbItems.push({ label: t('console.breadcrumb.dashboards'), href: `${breadcrumbItems[0].href}` });
114116
if (pathParts[3]) {
115117
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
116118
}
117119
} else if (routeType === 'page') {
118-
breadcrumbItems.push({ label: 'Pages', href: `${breadcrumbItems[0].href}` });
120+
breadcrumbItems.push({ label: t('console.breadcrumb.pages'), href: `${breadcrumbItems[0].href}` });
119121
if (pathParts[3]) {
120122
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
121123
}
122124
} else if (routeType === 'report') {
123-
breadcrumbItems.push({ label: 'Reports', href: `${breadcrumbItems[0].href}` });
125+
breadcrumbItems.push({ label: t('console.breadcrumb.reports'), href: `${breadcrumbItems[0].href}` });
124126
if (pathParts[3]) {
125127
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
126128
}
127129
} else if (routeType === 'system') {
128-
breadcrumbItems.push({ label: 'System' });
130+
breadcrumbItems.push({ label: t('console.breadcrumb.system') });
129131
if (pathParts[3]) {
130132
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
131133
}

apps/console/src/components/AppSidebar.tsx

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
393393
{/* Navigation Search */}
394394
<SidebarGroup className="py-0">
395395
<SidebarGroupContent className="relative">
396-
<Search className="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-50" />
396+
<Search className="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" />
397397
<SidebarInput
398398
placeholder="Search navigation..."
399399
value={navSearchQuery}
@@ -416,68 +416,68 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
416416
onReorder={handleReorder}
417417
/>
418418

419-
{/* Record Favorites */}
420-
{favorites.length > 0 && (
419+
{/* Recent Items (elevated position for quick access) */}
420+
{recentItems.length > 0 && (
421421
<SidebarGroup>
422-
<SidebarGroupLabel className="flex items-center gap-1.5">
423-
<Star className="h-3.5 w-3.5" />
424-
Favorites
422+
<SidebarGroupLabel
423+
className="flex items-center gap-1.5 cursor-pointer select-none"
424+
onClick={() => setRecentExpanded(prev => !prev)}
425+
>
426+
<ChevronRight className={`h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}`} />
427+
<Clock className="h-3.5 w-3.5" />
428+
Recent
425429
</SidebarGroupLabel>
430+
{recentExpanded && (
426431
<SidebarGroupContent>
427432
<SidebarMenu>
428-
{favorites.slice(0, 8).map(item => (
433+
{recentItems.slice(0, 5).map(item => (
429434
<SidebarMenuItem key={item.id}>
430435
<SidebarMenuButton asChild tooltip={item.label}>
431436
<Link to={item.href}>
432437
<span className="text-muted-foreground">
433-
{item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋'}
438+
{item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄'}
434439
</span>
435440
<span className="truncate">{item.label}</span>
436441
</Link>
437442
</SidebarMenuButton>
438-
<SidebarMenuAction
439-
showOnHover
440-
onClick={(e: any) => { e.stopPropagation(); removeFavorite(item.id); }}
441-
aria-label={`Remove ${item.label} from favorites`}
442-
>
443-
<StarOff className="h-3 w-3" />
444-
</SidebarMenuAction>
445443
</SidebarMenuItem>
446444
))}
447445
</SidebarMenu>
448446
</SidebarGroupContent>
447+
)}
449448
</SidebarGroup>
450449
)}
451450

452-
{/* Recent Items (default collapsed) */}
453-
{recentItems.length > 0 && (
451+
{/* Record Favorites */}
452+
{favorites.length > 0 && (
454453
<SidebarGroup>
455-
<SidebarGroupLabel
456-
className="flex items-center gap-1.5 cursor-pointer select-none"
457-
onClick={() => setRecentExpanded(prev => !prev)}
458-
>
459-
<ChevronRight className={`h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}`} />
460-
<Clock className="h-3.5 w-3.5" />
461-
Recent
454+
<SidebarGroupLabel className="flex items-center gap-1.5">
455+
<Star className="h-3.5 w-3.5" />
456+
Favorites
462457
</SidebarGroupLabel>
463-
{recentExpanded && (
464458
<SidebarGroupContent>
465459
<SidebarMenu>
466-
{recentItems.slice(0, 5).map(item => (
460+
{favorites.slice(0, 8).map(item => (
467461
<SidebarMenuItem key={item.id}>
468462
<SidebarMenuButton asChild tooltip={item.label}>
469463
<Link to={item.href}>
470464
<span className="text-muted-foreground">
471-
{item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄'}
465+
{item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋'}
472466
</span>
473467
<span className="truncate">{item.label}</span>
474468
</Link>
475469
</SidebarMenuButton>
470+
<SidebarMenuAction
471+
showOnHover
472+
onClick={(e: any) => { e.stopPropagation(); removeFavorite(item.id); }}
473+
aria-label={`Remove ${item.label} from favorites`}
474+
>
475+
<StarOff className="h-3 w-3" />
476+
</SidebarMenuAction>
476477
</SidebarMenuItem>
477478
))}
478479
</SidebarMenu>
479480
</SidebarGroupContent>
480-
)}
481481
</SidebarGroup>
482482
)}
483483
</>

packages/components/src/renderers/data-display/breadcrumb.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ComponentRegistry } from '@object-ui/core';
1010
import type { BreadcrumbSchema } from '@object-ui/types';
1111
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from '../../ui/breadcrumb';
1212
import { renderChildren } from '../../lib/utils';
13+
import { resolveI18nLabel } from '@object-ui/react';
1314

1415
ComponentRegistry.register('breadcrumb',
1516
({ schema, ...props }: { schema: BreadcrumbSchema; [key: string]: any }) => {
@@ -31,9 +32,9 @@ ComponentRegistry.register('breadcrumb',
3132
<div key={idx} className="flex items-center">
3233
<BreadcrumbItem>
3334
{idx === (schema.items?.length || 0) - 1 ? (
34-
<BreadcrumbPage>{item.label}</BreadcrumbPage>
35+
<BreadcrumbPage>{resolveI18nLabel(item.label) ?? ''}</BreadcrumbPage>
3536
) : (
36-
<BreadcrumbLink href={item.href}>{item.label}</BreadcrumbLink>
37+
<BreadcrumbLink href={item.href}>{resolveI18nLabel(item.label) ?? ''}</BreadcrumbLink>
3738
)}
3839
</BreadcrumbItem>
3940
{idx < (schema.items?.length || 0) - 1 && <BreadcrumbSeparator />}

packages/components/src/renderers/navigation/header-bar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import React from 'react';
1010
import { ComponentRegistry } from '@object-ui/core';
1111
import type { HeaderBarSchema } from '@object-ui/types';
12+
import { resolveI18nLabel } from '@object-ui/react';
1213
import {
1314
SidebarTrigger,
1415
Separator,
@@ -31,9 +32,9 @@ ComponentRegistry.register('header-bar',
3132
<React.Fragment key={idx}>
3233
<BreadcrumbItem>
3334
{idx === schema.crumbs.length - 1 ? (
34-
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
35+
<BreadcrumbPage>{resolveI18nLabel(crumb.label) ?? ''}</BreadcrumbPage>
3536
) : (
36-
<BreadcrumbLink href={crumb.href || '#'}>{crumb.label}</BreadcrumbLink>
37+
<BreadcrumbLink href={crumb.href || '#'}>{resolveI18nLabel(crumb.label) ?? ''}</BreadcrumbLink>
3738
)}
3839
</BreadcrumbItem>
3940
{idx < schema.crumbs.length - 1 && <BreadcrumbSeparator />}

packages/components/src/ui/sidebar.tsx

Lines changed: 80 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/i18n/src/locales/ar.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,12 @@ const ar = {
273273
console: {
274274
title: 'وحدة تحكم ObjectStack',
275275
initializing: 'جاري تهيئة التطبيق...',
276+
breadcrumb: {
277+
dashboards: 'لوحات المعلومات',
278+
pages: 'الصفحات',
279+
reports: 'التقارير',
280+
system: 'النظام',
281+
},
276282
loadingSteps: {
277283
connecting: 'جاري الاتصال بمصدر البيانات',
278284
loadingConfig: 'جاري تحميل الإعدادات',

packages/i18n/src/locales/de.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,12 @@ const de = {
272272
console: {
273273
title: 'ObjectStack Konsole',
274274
initializing: 'Anwendung wird initialisiert...',
275+
breadcrumb: {
276+
dashboards: 'Dashboards',
277+
pages: 'Seiten',
278+
reports: 'Berichte',
279+
system: 'System',
280+
},
275281
loadingSteps: {
276282
connecting: 'Verbindung zur Datenquelle herstellen',
277283
loadingConfig: 'Konfiguration laden',

0 commit comments

Comments
 (0)