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
14 changes: 14 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,20 @@ All 4 phases complete across 5 designers (Page, View, DataModel, Process, Report
- [ ] Column reorder/resize persistence for grid
- [ ] Drag-to-reschedule calendar events (move between dates)

#### P1.4 View Tab Management UX (Airtable/Salesforce-style)
- [x] `ViewTabBarConfig` type in `@object-ui/types` — configures tab bar UX (showAddButton, contextMenu, overflow, indicators)
- [x] `ViewTabBar` reusable component in `plugin-view` — extracted from console inline tabs
- [x] Inline "+" Add View button on tab bar (configurable via `showAddButton`)
- [x] Right-click context menu on view tabs: Rename, Duplicate, Delete, Set as Default, Share
- [x] Tab overflow → "More" dropdown when `maxVisibleTabs` exceeded
- [x] Filter/sort indicator badge (dot) on view tabs (`showIndicators`)
- [x] "Save as View" indicator + button when user has unsaved filter/sort changes
- [x] Double-click inline rename on view tabs (`inlineRename`)
- [ ] Drag-reorder view tabs (`reorderable` — needs `@dnd-kit/sortable`)
- [ ] Pin/favorite views
- [ ] View type quick-switch palette (Notion-style layout menu)
- [ ] Personal vs. shared views grouping in tab bar

### P2. Component & Plugin Excellence 🧩 (Completed Foundation)

**Goal:** Every component is polished, consistent, well-tested, and delightful to use.
Expand Down
56 changes: 33 additions & 23 deletions apps/console/src/components/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
import { ObjectChart } from '@object-ui/plugin-charts';
import { ListView } from '@object-ui/plugin-list';
import { DetailView } from '@object-ui/plugin-detail';
import { ObjectView as PluginObjectView } from '@object-ui/plugin-view';
import { ObjectView as PluginObjectView, ViewTabBar } from '@object-ui/plugin-view';
import type { ViewTabItem } from '@object-ui/plugin-view';
// Import plugins for side-effects (registration)
import '@object-ui/plugin-grid';
import '@object-ui/plugin-kanban';
Expand Down Expand Up @@ -358,30 +359,39 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
</div>
</div>

{/* View Tabs — Airtable-style named-view switcher */}
{/* View Tabs — Airtable-style named-view switcher with management UX */}
{views.length > 1 && (
<div className="border-b px-3 sm:px-4 bg-background overflow-x-auto shrink-0">
<div className="flex items-center gap-0.5 -mb-px">
{views.map((view: { id: string; label: string; type: string }) => {
const isActive = view.id === activeViewId;
const ViewIcon = VIEW_TYPE_ICONS[view.type] || TableIcon;
return (
<button
key={view.id}
onClick={() => handleViewChange(view.id)}
className={cn(
"inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap",
isActive
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
)}
>
<ViewIcon className="h-3.5 w-3.5" />
{view.label}
</button>
);
})}
</div>
<ViewTabBar
views={views.map((view: { id: string; label: string; type: string; filter?: any[]; sort?: any[] }) => ({
id: view.id,
label: view.label,
type: view.type,
hasActiveFilters: Array.isArray((view as any).filter) && (view as any).filter.length > 0,
hasActiveSort: Array.isArray((view as any).sort) && (view as any).sort.length > 0,
Comment on lines +370 to +371

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The views.map(...) callback is already typed with optional filter/sort, but the code still casts to any to read them. Using view.filter / view.sort directly would keep this type-safe and avoid unnecessary any casts.

Suggested change
hasActiveFilters: Array.isArray((view as any).filter) && (view as any).filter.length > 0,
hasActiveSort: Array.isArray((view as any).sort) && (view as any).sort.length > 0,
hasActiveFilters: Array.isArray(view.filter) && view.filter.length > 0,
hasActiveSort: Array.isArray(view.sort) && view.sort.length > 0,

Copilot uses AI. Check for mistakes.
} as ViewTabItem))}
activeViewId={activeViewId}
onViewChange={handleViewChange}
viewTypeIcons={VIEW_TYPE_ICONS}
config={objectDef.viewTabBar}
onAddView={() => navigate(viewId ? '../../views/new' : 'views/new')}
onRenameView={(id, newName) => {
// Rename is wired for future backend integration
console.info('[ViewTabBar] Rename view:', id, newName);
}}
onDuplicateView={(id) => {
console.info('[ViewTabBar] Duplicate view:', id);
}}
onDeleteView={(id) => {
console.info('[ViewTabBar] Delete view:', id);
}}
onSetDefaultView={(id) => {
console.info('[ViewTabBar] Set default view:', id);
}}
onShareView={(id) => {
console.info('[ViewTabBar] Share view:', id);
}}
/>
</div>
)}

Expand Down
Loading