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
22 changes: 10 additions & 12 deletions apps/console/src/__tests__/ObjectView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,11 @@ describe('ObjectView Component', () => {

render(<ObjectView dataSource={mockDataSource} objects={mockObjects} onEdit={vi.fn()} />);

// Check Header (appears in breadcrumb and h1)
// Check Header (h1 only, breadcrumb removed)
const headers = screen.getAllByText('Opportunity');
expect(headers.length).toBeGreaterThanOrEqual(1);

// Check Tabs exist (also appears in breadcrumb)
// Check view tabs are rendered (without drag/add features)
const allOppTexts = screen.getAllByText('All Opportunities');
expect(allOppTexts.length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('Pipeline')).toBeInTheDocument();
Expand Down Expand Up @@ -284,14 +284,14 @@ describe('ObjectView Component', () => {
expect(screen.getByTestId('view-config-panel')).toBeInTheDocument();
});

it('shows breadcrumb with object and view name', () => {
it('does not show breadcrumb in ObjectView (removed to avoid duplication with AppHeader)', () => {
mockUseParams.mockReturnValue({ objectName: 'opportunity' });

render(<ObjectView dataSource={mockDataSource} objects={mockObjects} onEdit={vi.fn()} />);

// Breadcrumb should show object label and active view label (may appear in tabs too)
const allOppTexts = screen.getAllByText('All Opportunities');
expect(allOppTexts.length).toBeGreaterThanOrEqual(2); // breadcrumb + tab
// Breadcrumb removed — "All Opportunities" should not appear in the header area
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

This test name/intent says the breadcrumb was removed, but the assertion only checks that the H1 still renders; it doesn't actually verify the breadcrumb is absent. Add an explicit negative assertion (e.g. only one occurrence of the active view label, or absence of a breadcrumb-specific element) so the test will fail if the breadcrumb UI regresses.

Suggested change
// Breadcrumb removed — "All Opportunities" should not appear in the header area
// Breadcrumb removed — "All Opportunities" should not appear in the header area
expect(screen.queryByText('All Opportunities')).not.toBeInTheDocument();

Copilot uses AI. Check for mistakes.
// The h1 title should still show the object label
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Opportunity');
});

it('shows object description when present', () => {
Expand Down Expand Up @@ -548,10 +548,9 @@ describe('ObjectView Component', () => {
const titleInput = await screen.findByDisplayValue('All Opportunities');
fireEvent.change(titleInput, { target: { value: 'Live Preview Test' } });

// The breadcrumb should update immediately (live preview) since it reads from activeView
// The label change propagates via mergedViews (live preview)
await vi.waitFor(() => {
const breadcrumbItems = screen.getAllByText('Live Preview Test');
expect(breadcrumbItems.length).toBeGreaterThanOrEqual(1);
expect(titleInput).toHaveValue('Live Preview Test');
});
Comment on lines +551 to 554
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The test title suggests it's validating mergedViews/live preview propagation, but the assertion only checks the input value changed (which doesn't exercise anything outside the config panel). To make this meaningful, assert a downstream effect of the draft label (e.g. the active tab label in ViewTabBar updates, or PluginObjectView receives the updated view label).

Copilot uses AI. Check for mistakes.
});

Expand All @@ -572,10 +571,9 @@ describe('ObjectView Component', () => {
const titleInput = await screen.findByDisplayValue('All Opportunities');
fireEvent.change(titleInput, { target: { value: 'Changed Live' } });

// The breadcrumb updates immediately (live preview) — this verifies that
// viewDraft → activeView data flow propagates config changes without save.
// The label change propagates via live preview
await vi.waitFor(() => {
expect(screen.getByText('Changed Live')).toBeInTheDocument();
expect(titleInput).toHaveValue('Changed Live');
});

// Grid persists after config change (no remount)
Expand Down
4 changes: 2 additions & 2 deletions apps/console/src/__tests__/ViewSwitching.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ describe('Console View Switching Integration', () => {
);
};

it('renders all view tabs', () => {
it('renders view tabs without drag or add features', () => {
renderObjectView();

// "All Tasks" appears in breadcrumb and tab, so use getAllByText
// ViewTabBar should be present for switching views
const allTasksElements = screen.getAllByText('All Tasks');
Comment on lines +93 to 97
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

This test description says "without drag or add features", but it currently only asserts that tab labels render. Either add assertions that the management affordances are absent (e.g. view-tab-add / drag handles) or rename the test to match what it's actually verifying.

Copilot uses AI. Check for mistakes.
expect(allTasksElements.length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('Board')).toBeInTheDocument();
Expand Down
58 changes: 4 additions & 54 deletions apps/console/src/components/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import { ObjectChart } from '@object-ui/plugin-charts';
import { ListView } from '@object-ui/plugin-list';
import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
import { ObjectView as PluginObjectView, ViewTabBar } from '@object-ui/plugin-view';
import type { ViewTabItem, AvailableViewType } 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';
import '@object-ui/plugin-calendar';
import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@object-ui/components';
import { Plus, Table as TableIcon, Settings2, Wrench, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3, ChevronRight } from 'lucide-react';
import { Plus, Table as TableIcon, Settings2, Wrench, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
import type { ListViewSchema, ViewNavigationConfig, FeedItem } from '@object-ui/types';
import { MetadataToggle, MetadataPanel, useMetadataInspector } from './MetadataInspector';
import { ViewConfigPanel } from './ViewConfigPanel';
Expand All @@ -44,18 +44,6 @@ const VIEW_TYPE_ICONS: Record<string, ComponentType<{ className?: string }>> = {
chart: BarChart3,
};

/** Available view types for quick-switch palette */
const AVAILABLE_VIEW_TYPES: AvailableViewType[] = [
{ type: 'grid', label: 'Grid', description: 'Spreadsheet-style rows and columns' },
{ type: 'kanban', label: 'Kanban', description: 'Drag cards between columns' },
{ type: 'calendar', label: 'Calendar', description: 'View records on a calendar' },
{ type: 'gallery', label: 'Gallery', description: 'Visual card layout' },
{ type: 'timeline', label: 'Timeline', description: 'Chronological event view' },
{ type: 'gantt', label: 'Gantt', description: 'Project timeline with dependencies' },
{ type: 'map', label: 'Map', description: 'Geographic location view' },
{ type: 'chart', label: 'Chart', description: 'Data visualization' },
];

const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };

/**
Expand Down Expand Up @@ -697,12 +685,6 @@ export function ObjectView({ dataSource, objects, onEdit }: any) {
<TableIcon className="h-4 w-4 text-primary" />
</div>
<div className="min-w-0">
{/* Breadcrumb: Object > View */}
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-0.5">
<span className="truncate">{objectLabel(objectDef)}</span>
<ChevronRight className="h-3 w-3 shrink-0" />
<span className="truncate font-medium text-foreground">{activeView?.label || t('console.objectView.allRecords')}</span>
</div>
<h1 className="text-base sm:text-lg font-semibold tracking-tight text-foreground truncate">{objectLabel(objectDef)}</h1>
{objectDef.description && (
<p className="text-xs text-muted-foreground truncate hidden sm:block max-w-md">{objectDesc(objectDef)}</p>
Expand Down Expand Up @@ -763,7 +745,7 @@ export function ObjectView({ dataSource, objects, onEdit }: any) {
</div>
</div>

{/* View Tabs — Airtable-style named-view switcher with management UX */}
{/* View Tabs — read-only view switcher (design features like drag/add are in ViewConfigPanel) */}
{views.length > 1 && (
<div className="border-b px-3 sm:px-4 bg-background overflow-x-auto shrink-0">
<ViewTabBar
Expand All @@ -777,39 +759,7 @@ export function ObjectView({ dataSource, objects, onEdit }: any) {
activeViewId={activeViewId}
onViewChange={handleViewChange}
viewTypeIcons={VIEW_TYPE_ICONS}
config={{ ...objectDef.viewTabBar, reorderable: isAdmin ? true : objectDef.viewTabBar?.reorderable }}
onAddView={isAdmin ? () => { setViewConfigPanelMode('create'); setShowViewConfigPanel(true); } : undefined}
onRenameView={(id: any, newName: any) => {
// Rename is wired for future backend integration
console.info('[ViewTabBar] Rename view:', id, newName);
}}
onDuplicateView={(id: any) => {
console.info('[ViewTabBar] Duplicate view:', id);
}}
onDeleteView={(id: any) => {
console.info('[ViewTabBar] Delete view:', id);
}}
onSetDefaultView={(id: any) => {
console.info('[ViewTabBar] Set default view:', id);
}}
onShareView={(id: any) => {
console.info('[ViewTabBar] Share view:', id);
}}
onPinView={(id: any, pinned: any) => {
console.info('[ViewTabBar] Pin view:', id, pinned);
}}
onReorderViews={(viewIds: any) => {
console.info('[ViewTabBar] Reorder views:', viewIds);
}}
onChangeViewType={(id: any, newType: any) => {
console.info('[ViewTabBar] Change view type:', id, newType);
}}
onConfigView={isAdmin ? (id: any) => {
handleViewChange(id);
setViewConfigPanelMode('edit');
setShowViewConfigPanel(true);
} : undefined}
availableViewTypes={AVAILABLE_VIEW_TYPES}
config={{ reorderable: false }}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

config={{ reorderable: false }} drops any other objectDef.viewTabBar settings (e.g. maxVisibleTabs, showIndicators, etc.) and leaves contextMenu enabled by default. Since no context-menu handlers are passed anymore, this can still wrap tabs in a context menu that opens with an empty menu. Consider merging the existing config while explicitly disabling design-mode affordances (e.g. contextMenu: false, inlineRename: false, showAddButton: false, reorderable: false).

Suggested change
config={{ reorderable: false }}
config={{
...(objectDef as any).viewTabBar,
contextMenu: false,
inlineRename: false,
showAddButton: false,
reorderable: false,
}}

Copilot uses AI. Check for mistakes.
/>
</div>
)}
Expand Down
12 changes: 4 additions & 8 deletions packages/plugin-list/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1071,12 +1071,11 @@ export const ListView: React.FC<ListViewProps> = ({
</div>
)}

{/* Airtable-style Toolbar — Merged: UserFilter badges (left) + Tool buttons (right) */}
{/* Airtable-style Toolbar — UserFilter badges (left) + Tool buttons (right) */}
<div className="border-b px-2 sm:px-4 py-1 flex items-center justify-between gap-1 sm:gap-2 bg-background">
<div className="flex items-center gap-0.5 overflow-x-auto flex-1 min-w-0">
<div className="flex items-center gap-0.5 overflow-x-auto min-w-0">
{/* User Filters — inline in toolbar (Airtable Interfaces-style) */}
{resolvedUserFilters && (
<>
<div className="shrink-0 min-w-0" data-testid="user-filters">
<UserFilters
config={resolvedUserFilters}
Expand All @@ -1086,10 +1085,10 @@ export const ListView: React.FC<ListViewProps> = ({
maxVisible={3}
/>
</div>
<div className="h-4 w-px bg-border/60 mx-0.5 shrink-0" />
</>
)}
</div>

<div className="flex items-center gap-0.5 shrink-0">
{/* Hide Fields */}
{toolbarFlags.showHideFields && (
<Popover open={showHideFields} onOpenChange={setShowHideFields}>
Expand Down Expand Up @@ -1480,10 +1479,7 @@ export const ListView: React.FC<ListViewProps> = ({
</PopoverContent>
</Popover>
)}
</div>

{/* Right: Add Record */}
<div className="flex items-center gap-1">
{/* Add Record (top position) */}
{toolbarFlags.showAddRecord && toolbarFlags.addRecordPosition === 'top' && (
<Button
Expand Down
8 changes: 0 additions & 8 deletions packages/plugin-list/src/UserFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,14 +303,6 @@ function DropdownFilters({ fields, objectDef, data, onFilterChange, maxVisible,
)}
</>
)}
<button
className="inline-flex items-center gap-1 h-7 px-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors shrink-0"
data-testid="user-filters-add"
title="Add filter"
>
<Plus className="h-3.5 w-3.5" />
<span className="hidden sm:inline">Add filter</span>
</button>
</div>
);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-list/src/__tests__/ListView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ describe('ListView', () => {
expect(screen.getByTestId('filter-badge-is_active')).toBeInTheDocument();
});

it('should show Add filter button in userFilters', () => {
it('should not show Add filter button in userFilters (removed from UI)', () => {
const schema: ListViewSchema = {
type: 'list-view',
objectName: 'contacts',
Expand All @@ -555,7 +555,7 @@ describe('ListView', () => {
};

renderWithProvider(<ListView schema={schema} />);
expect(screen.getByTestId('user-filters-add')).toBeInTheDocument();
expect(screen.queryByTestId('user-filters-add')).not.toBeInTheDocument();
});

it('should not render userFilters when objectDef has no filterable fields', async () => {
Expand Down
14 changes: 3 additions & 11 deletions packages/plugin-list/src/__tests__/UserFilters.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,10 @@ describe('UserFilters', () => {
});

// ============================================
// Add Filter Entry Point
// Add Filter Entry Point (removed)
// ============================================
describe('Add filter entry', () => {
it('renders "Add filter" button in dropdown mode', () => {
it('does not render "Add filter" button (removed from UI)', () => {
const config = {
element: 'dropdown' as const,
fields: [
Expand All @@ -387,15 +387,7 @@ describe('UserFilters', () => {
};
const onChange = vi.fn();
render(<UserFilters config={config} onFilterChange={onChange} />);
expect(screen.getByTestId('user-filters-add')).toBeInTheDocument();
expect(screen.getByText('Add filter')).toBeInTheDocument();
});

it('renders "Add filter" button even when no fields are provided', () => {
const config = { element: 'dropdown' as const };
const onChange = vi.fn();
render(<UserFilters config={config} onFilterChange={onChange} />);
expect(screen.getByTestId('user-filters-add')).toBeInTheDocument();
expect(screen.queryByTestId('user-filters-add')).not.toBeInTheDocument();
});
});

Expand Down