diff --git a/ROADMAP.md b/ROADMAP.md
index 8693e3005..59777de9c 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1087,6 +1087,18 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
All 313 `@object-ui/fields` tests pass.
+### ListView Airtable-Style Toolbar Opt-In & Duplicate Record Count (February 2026)
+
+**Root Cause (1 — Toolbar defaults):** `showHideFields`, `showColor`, and `showDensity` in `ListView.tsx` used opt-out logic (`!== false`), making secondary toolbar buttons visible by default. Airtable hides these controls unless explicitly enabled.
+
+**Fix:** Changed default logic from `!== false` (opt-out) to `=== true` (opt-in) for `showHideFields`, `showColor`, and `showDensity` in the `toolbarFlags` computation. Updated `@default` JSDoc comments in `NamedListView` and `ListViewSchema` interfaces from `@default true` to `@default false`.
+
+**Root Cause (2 — Duplicate record count):** Both `ListView.tsx` (`record-count-bar`) and `ObjectView.tsx` (`record-count-footer`) independently rendered the record count at the bottom, causing duplicate display.
+
+**Fix:** Removed the `record-count-footer` from `ObjectView.tsx` since `ListView` already renders the authoritative `record-count-bar`.
+
+**Tests:** Updated 11 tests across `ListView.test.tsx` and `ObjectView.test.tsx`. All 112 ListView tests and 32 ObjectView tests pass.
+
---
## ⚠️ Risk Management
diff --git a/apps/console/src/__tests__/ObjectView.test.tsx b/apps/console/src/__tests__/ObjectView.test.tsx
index 1c184463a..29d54d9b7 100644
--- a/apps/console/src/__tests__/ObjectView.test.tsx
+++ b/apps/console/src/__tests__/ObjectView.test.tsx
@@ -358,7 +358,7 @@ describe('ObjectView Component', () => {
expect(screen.queryByTestId('view-config-panel')).not.toBeInTheDocument();
});
- it('shows record count footer when data is available', async () => {
+ it('does not render duplicate record count footer (ListView handles it)', async () => {
const mockDsWithTotal = {
...mockDataSource,
find: vi.fn().mockResolvedValue({ data: [], total: 42 }),
@@ -367,9 +367,11 @@ describe('ObjectView Component', () => {
render();
- // Wait for the record count to appear
- const footer = await screen.findByTestId('record-count-footer');
- expect(footer).toBeInTheDocument();
+ // The record-count-footer should no longer exist in ObjectView
+ // (ListView's record-count-bar handles this)
+ await vi.waitFor(() => {
+ expect(screen.queryByTestId('record-count-footer')).not.toBeInTheDocument();
+ });
});
it('calls dataSource.updateViewConfig when saving view config', async () => {
diff --git a/apps/console/src/components/ObjectView.tsx b/apps/console/src/components/ObjectView.tsx
index 19f66edd9..d9f247b6c 100644
--- a/apps/console/src/components/ObjectView.tsx
+++ b/apps/console/src/components/ObjectView.tsx
@@ -782,12 +782,7 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
renderListView={renderListView}
/>
- {/* Footer — Record count */}
- {typeof recordCount === 'number' && (
-
- {t('console.objectView.recordCount', { count: recordCount })}
-
- )}
+ {/* Record count footer removed — ListView already renders record-count-bar */}
{/* Metadata panel only shows for admin users */}
= ({
showSearch: ua?.search !== undefined ? ua.search : schema.showSearch !== false,
showSort: ua?.sort !== undefined ? ua.sort : schema.showSort !== false,
showFilters: ua?.filter !== undefined ? ua.filter : schema.showFilters !== false,
- showDensity: ua?.rowHeight !== undefined ? ua.rowHeight : schema.showDensity !== false,
- showHideFields: schema.showHideFields !== false,
+ showDensity: ua?.rowHeight !== undefined ? ua.rowHeight : schema.showDensity === true,
+ showHideFields: schema.showHideFields === true,
showGroup: schema.showGroup !== false,
- showColor: schema.showColor !== false,
+ showColor: schema.showColor === true,
showAddRecord: addRecordEnabled,
addRecordPosition: (schema.addRecord?.position === 'bottom' ? 'bottom' : 'top') as 'top' | 'bottom',
};
diff --git a/packages/plugin-list/src/__tests__/ListView.test.tsx b/packages/plugin-list/src/__tests__/ListView.test.tsx
index fb85babd4..6dfd4b2b0 100644
--- a/packages/plugin-list/src/__tests__/ListView.test.tsx
+++ b/packages/plugin-list/src/__tests__/ListView.test.tsx
@@ -279,6 +279,7 @@ describe('ListView', () => {
objectName: 'contacts',
viewType: 'grid',
fields: ['name', 'email', 'phone'],
+ showHideFields: true,
};
renderWithProvider();
@@ -293,6 +294,7 @@ describe('ListView', () => {
objectName: 'contacts',
viewType: 'grid',
fields: ['name', 'email'],
+ showDensity: true,
};
renderWithProvider();
@@ -353,6 +355,7 @@ describe('ListView', () => {
viewType: 'grid',
fields: ['name', 'email'],
rowHeight: 'compact',
+ showDensity: true,
};
renderWithProvider();
@@ -368,6 +371,7 @@ describe('ListView', () => {
fields: ['name', 'email'],
rowHeight: 'compact',
densityMode: 'spacious',
+ showDensity: true,
};
renderWithProvider();
@@ -699,7 +703,7 @@ describe('ListView', () => {
expect(screen.queryByRole('button', { name: /hide fields/i })).not.toBeInTheDocument();
});
- it('should show Hide Fields button by default (showHideFields undefined)', () => {
+ it('should hide Hide Fields button by default (showHideFields undefined)', () => {
const schema: ListViewSchema = {
type: 'list-view',
objectName: 'contacts',
@@ -708,7 +712,7 @@ describe('ListView', () => {
};
renderWithProvider();
- expect(screen.getByRole('button', { name: /hide fields/i })).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /hide fields/i })).not.toBeInTheDocument();
});
// Group visibility
@@ -751,7 +755,7 @@ describe('ListView', () => {
expect(screen.queryByRole('button', { name: /color/i })).not.toBeInTheDocument();
});
- it('should show Color button by default (showColor undefined)', () => {
+ it('should hide Color button by default (showColor undefined)', () => {
const schema: ListViewSchema = {
type: 'list-view',
objectName: 'contacts',
@@ -760,7 +764,7 @@ describe('ListView', () => {
};
renderWithProvider();
- expect(screen.getByRole('button', { name: /color/i })).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /color/i })).not.toBeInTheDocument();
});
// Density visibility
@@ -777,7 +781,7 @@ describe('ListView', () => {
expect(screen.queryByTitle(/density/i)).not.toBeInTheDocument();
});
- it('should show Density button by default (showDensity undefined)', () => {
+ it('should hide Density button by default (showDensity undefined)', () => {
const schema: ListViewSchema = {
type: 'list-view',
objectName: 'contacts',
@@ -786,7 +790,7 @@ describe('ListView', () => {
};
renderWithProvider();
- expect(screen.getByTitle(/density/i)).toBeInTheDocument();
+ expect(screen.queryByTitle(/density/i)).not.toBeInTheDocument();
});
// Export + allowExport
@@ -925,6 +929,7 @@ describe('ListView', () => {
viewType: 'grid',
fields: ['name', 'email'],
rowHeight: 'short',
+ showDensity: true,
};
renderWithProvider();
@@ -939,6 +944,7 @@ describe('ListView', () => {
viewType: 'grid',
fields: ['name', 'email'],
rowHeight: 'extra_tall',
+ showDensity: true,
};
renderWithProvider();
@@ -1595,6 +1601,7 @@ describe('ListView', () => {
objectName: 'contacts',
viewType: 'grid',
fields: ['name', 'email'],
+ showColor: true,
};
renderWithProvider();
@@ -1609,6 +1616,7 @@ describe('ListView', () => {
objectName: 'contacts',
viewType: 'grid',
fields: ['name', 'email'],
+ showColor: true,
};
renderWithProvider();
diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts
index 72c3cabd8..9f9265ec5 100644
--- a/packages/types/src/objectql.ts
+++ b/packages/types/src/objectql.ts
@@ -1119,16 +1119,16 @@ export interface NamedListView {
/** Show filter controls in toolbar @default true */
showFilters?: boolean;
- /** Show hide-fields button in toolbar @default true */
+ /** Show hide-fields button in toolbar @default false */
showHideFields?: boolean;
/** Show group button in toolbar @default true */
showGroup?: boolean;
- /** Show color button in toolbar @default true */
+ /** Show color button in toolbar @default false */
showColor?: boolean;
- /** Show density/row-height button in toolbar @default true */
+ /** Show density/row-height button in toolbar @default false */
showDensity?: boolean;
/** Allow data export @default undefined */
@@ -1394,16 +1394,16 @@ export interface ListViewSchema extends BaseSchema {
/** Show filter controls in toolbar @default true */
showFilters?: boolean;
- /** Show hide-fields button in toolbar @default true */
+ /** Show hide-fields button in toolbar @default false */
showHideFields?: boolean;
/** Show group button in toolbar @default true */
showGroup?: boolean;
- /** Show color button in toolbar @default true */
+ /** Show color button in toolbar @default false */
showColor?: boolean;
- /** Show density/row-height button in toolbar @default true */
+ /** Show density/row-height button in toolbar @default false */
showDensity?: boolean;
/** Allow data export @default undefined */