Skip to content

feat: Airtable Interfaces-style userFilters (dropdown/tabs/toggle) for ListView#638

Merged
hotlong merged 3 commits intomainfrom
copilot/add-user-filters-to-listview
Feb 20, 2026
Merged

feat: Airtable Interfaces-style userFilters (dropdown/tabs/toggle) for ListView#638
hotlong merged 3 commits intomainfrom
copilot/add-user-filters-to-listview

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 20, 2026

Adds userFilters configuration to ListViewSchema, supporting three Airtable Interfaces-style filter display modes: dropdown badges, tab presets, and toggle buttons.

Schema (@object-ui/types)

  • New userFilters property on ListViewSchema with element: 'dropdown' | 'tabs' | 'toggle'
  • Field-level filter definitions (fields[]) for dropdown/toggle modes with options, colors, counts, defaults
  • Named filter presets (tabs[]) for tabs mode with "All records" and add-tab support
  • Corresponding Zod validators: UserFilterOptionSchema, UserFilterFieldSchema, UserFilterTabSchema, UserFiltersSchema

Component (@object-ui/plugin-list)

  • New UserFilters.tsx — dispatches to DropdownFilters, TabFilters, or ToggleFilters based on config.element
  • Dropdown: popover multi-select per field, color dots, option counts, clear button
  • Tabs: tab bar with active state, optional "All records" tab, optional add button
  • Toggle: on/off buttons per field using defaultValues or != null conditions
  • Auto-derives field options from objectDef when options not provided statically

ListView integration

  • userFilterConditions state merged into the data fetch filter pipeline alongside base/quick/builder filters
  • Renders below quick filters row, backward-compatible — existing quickFilters unaffected

Usage

{
  "type": "list-view",
  "objectName": "accounts",
  "userFilters": {
    "element": "dropdown",
    "fields": [
      { "field": "status", "label": "Status", "type": "multi-select", "showCount": true },
      { "field": "priority", "label": "Priority", "options": [
        { "label": "High", "value": "high", "color": "#dc2626" }
      ]}
    ]
  }
}

Tests

  • 25 tests covering all three modes (render, interaction, defaults, edge cases)
  • 5 Zod validation tests (dropdown/tabs/toggle/invalid/backward-compat)
Original prompt

This section details on the original issue you should resolve

<issue_title>🎯 Feature: Airtable Interfaces 风格 User Filters — 列表视图顶部过滤器(Dropdown / Tabs / Toggle 三模式)</issue_title>
<issue_description>参考 Airtable Interfaces 的 User Filters 设计,为 ListView 工具栏新增 userFilters 配置,支持三种 Element 呈现模式:

  • Dropdown — 每个可过滤字段显示为独立的下拉选择器 badge(如 状态 ∨
  • Tabs — 预设过滤组合显示为选项卡(如 Tab | my customers | All records
  • Toggle — 过滤字段显示为开关切换按钮

Airtable 参考截图

Dropdown 模式 — 工具栏左侧显示字段级过滤 badge:
image1

Tabs 模式 + 设置面板 — 右侧面板中 Elements 和 Tabs 配置:
image2

当前状态

  • ListView.tsx 已有 Airtable 风格工具栏(Hide fields / Filter / Group / Sort / Color / Density / Search)
  • quickFilters 仅支持 toggle button 模式,无 dropdown 和 tabs 模式
  • 工具栏左侧无字段级过滤 badge,右侧工具按钮无法自动收纳

涉及的包和文件

文件 变更类型
@object-ui/types packages/types/src/objectql.ts Schema 扩展 — 新增 userFilters 属性
@object-ui/types packages/types/src/zod/objectql.zod.ts Zod schema 验证
@object-ui/plugin-list packages/plugin-list/src/UserFilters.tsx 新文件 — 三模式渲染组件
@object-ui/plugin-list packages/plugin-list/src/ListView.tsx 工具栏集成 + 状态管理
@object-ui/plugin-list packages/plugin-list/src/__tests__/UserFilters.test.tsx 新文件 — 单元测试

Schema 设计

// ListViewSchema extension
userFilters?: {
  /** UI element type: 'dropdown' | 'tabs' | 'toggle' */
  element: 'dropdown' | 'tabs' | 'toggle';

  /** Field-level filters (for dropdown & toggle modes) */
  fields?: Array<{
    field: string;
    label?: string;
    type?: 'select' | 'multi-select' | 'boolean' | 'date-range' | 'text';
    options?: Array<{ label: string; value: string | number | boolean; color?: string }>;
    showCount?: boolean;
    defaultValues?: (string | number | boolean)[];
  }>;

  /** Named filter presets (for tabs mode) */
  tabs?: Array<{
    id: string;
    label: string;
    filters: Array<any[] | string>;
    icon?: string;
    default?: boolean;
  }>;

  allowAddTab?: boolean;
  showAllRecords?: boolean;
};

使用示例

Dropdown 模式:

{
  "type": "list-view",
  "objectName": "accounts",
  "userFilters": {
    "element": "dropdown",
    "fields": [
      { "field": "status", "label": "状态", "type": "multi-select", "showCount": true },
      { "field": "priority_tag", "label": "重要性标签", "type": "multi-select",
        "options": [
          { "label": "Enterprise", "value": "enterprise", "color": "#dc2626" },
          { "label": "SMB", "value": "smb", "color": "#2563eb" }
        ]
      }
    ]
  }
}

Tabs 模式:

{
  "type": "list-view",
  "objectName": "accounts",
  "userFilters": {
    "element": "tabs",
    "showAllRecords": true,
    "allowAddTab": true,
    "tabs": [
      { "id": "tab-1", "label": "Tab", "filters": [["status", "=", "active"]], "default": true },
      { "id": "tab-2", "label": "my customers", "filters": [["owner", "=", "$currentUser"]] }
    ]
  }
}

验收标准

  • Schema: userFilters 类型定义完整,Zod 验证通过
  • Dropdown 模式: 渲染字段级 badge,点击展开选项列表,支持多选,显示计数
  • Tabs 模式: 渲染选项卡栏,切换 tab 应用对应过滤条件,支持 "All records" tab
  • Toggle 模式: 渲染开关按钮,点击切换激活状态
  • 数据联动: 选择过滤器后自动重新获取数据
  • 自动推导: 从 objectDef 自动推导字段选项(当未提供 options 时)
  • 向后兼容: 现有 quickFilters 继续工作
  • 测试: 覆盖三种模式的渲染、交互、数据联动
  • Roadmap: 更新 ROADMAP.md 标记此功能为 v1.0 UI Essentials 进行中

子任务

  • Sub-issue 1: Schema 扩展 + Zod 验证 (@object-ui/types)
  • Sub-issue 2: UserFilters 三模式渲染组件 (@object-ui/plugin-list)
  • Sub-issue 3: ListView 工具栏集成 + 测试 + Roadmap 更新

优先级

🔴 P0 — v1.0 UI Essentials,直接影响列表视图的用户体验,对标 Airtable Interfaces 核心交互模式

🔍 截图关键信息解读

从右侧配置面板可以看到:

User Filters 配置区域

配置项 含义
Elements 三个图标按钮(👁 / 📺 / 👁‍🗨) 过滤器的 展示模式:隐藏 / 下拉框(Dropdown) / 选项卡(Tab)
Tabs Tab, my customer... ⚙️ Tab 模式下的具体标签页配置

User Actions 配置区域

配置项 状态
Sort ✅ 开启
Search ✅ 开启
Filter ✅ 开启
Group ✅ 开启
Row height ✅ 开启

左侧实际效果

工具栏显示为:Tab | my customers | All records ➕ — 这就是 Tab 模式 的过滤器,而不是下拉框模式。


📐 Airtable 的两种 User Filter 模式

从截图分析,Airtable 的 Display Filter 支持三种 Element 模式:

  1. Hidden (👁) — 不显示过滤器
  2. Dropdown (📺) — 以下拉选择框形式展示(即之前截图中的 状态 ∨ 重要性标签 ∨
  3. Tab (👁‍🗨) — 以选项卡形式展示(即当前截图中的 Tab | my customers | All records

🏗️ ObjectUI 中的实现方案

让我先检查现有的相关实现基础:Now let me look at how the ObjectView handles the right-side settings panel pattern shown in the screenshot:根据你提供的两张截图的深入分析,我现在清晰理解了 Airtable Interfaces 的 User Filters 双模式设计。让我做详细的对比和方案��计。


🔍 Airtable Interfaces User Filters 双模式解析

从截图 image2 右侧的设置面板可以清楚看到 Airtable 的 User Filters 配置系统:

设置项 截图中的值 含义
Elements 三个图标切换(👁️ 下拉框 / 📺 选项卡 / 🔘 切换) 过滤器的 UI 呈现模式
Tabs Tab, my customer... ⚙️ 选项卡的具体配置(名称、过滤条件)

对照截图 image1 工具栏左侧的 状态 ∨ 重要性标签 ∨

  • **模式1:Dro...

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Error Error Feb 20, 2026 2:06am
objectui-demo Error Error Feb 20, 2026 2:06am
objectui-storybook Error Error Feb 20, 2026 2:06am

Request Review

Copilot AI and others added 2 commits February 20, 2026 02:00
- Add userFilters type definition to ListViewSchema in @object-ui/types
- Add Zod validation schema for userFilters configuration
- Create UserFilters component with 3 rendering modes (dropdown/tabs/toggle)
- Integrate UserFilters into ListView toolbar with data refetch
- Export UserFilters from plugin-list
- Add 25 unit tests for UserFilters component
- Add 5 Zod validation tests for userFilters schema
- Update ROADMAP.md to mark userFilters as completed

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add user filters with dropdown, tabs, and toggle modes feat: Airtable Interfaces-style userFilters (dropdown/tabs/toggle) for ListView Feb 20, 2026
Copilot AI requested a review from hotlong February 20, 2026 02:04
@hotlong hotlong marked this pull request as ready for review February 20, 2026 04:25
Copilot AI review requested due to automatic review settings February 20, 2026 04:25
@hotlong hotlong merged commit 8ca1d97 into main Feb 20, 2026
2 of 5 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds Airtable Interfaces-style user filters to ListView, supporting three distinct filter presentation modes: dropdown (field-level badges), tabs (named filter presets), and toggle (on/off buttons). The implementation spans schema definitions, component logic, and comprehensive test coverage.

Changes:

  • Extends ListViewSchema with new userFilters property supporting dropdown/tabs/toggle modes
  • Implements UserFilters component with mode-specific renderers and auto-derivation of field options from objectDef
  • Integrates user filters into ListView's data fetching pipeline alongside existing quickFilters and FilterBuilder
  • Adds 25 component tests and 5 Zod validation tests covering all modes and edge cases
  • Updates ROADMAP to mark P0.4 ListView spec property as complete

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/types/src/objectql.ts Adds userFilters TypeScript interface with detailed JSDoc for three element modes (dropdown/tabs/toggle)
packages/types/src/zod/objectql.zod.ts Defines Zod validators for UserFilterOption, UserFilterField, UserFilterTab, and UserFilters schemas
packages/types/src/tests/phase2-schemas.test.ts Adds 5 validation tests covering dropdown, tabs, toggle modes, invalid input, and backward compatibility
packages/plugin-list/src/UserFilters.tsx Implements main UserFilters component with three mode-specific renderers (DropdownFilters, TabFilters, ToggleFilters) and shared option resolution logic
packages/plugin-list/src/tests/UserFilters.test.tsx Comprehensive test suite with 25 tests covering rendering, interaction, multi-select, counts, colors, defaults, and edge cases
packages/plugin-list/src/index.tsx Exports UserFilters component and UserFiltersProps type for public API
packages/plugin-list/src/ListView.tsx Integrates userFilters state into data fetching pipeline, renders UserFilters component below quickFilters row, passes objectDef and data props
ROADMAP.md Marks userFilters spec property implementation as complete in P0.4 section

Comment on lines +123 to +126
if (f.showCount && data.length > 0) {
options = options.map(opt => ({
...opt,
count: data.filter(row => row[f.field] === opt.value).length,
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The record count calculation on line 126 uses the filtered data array, which means counts will reflect already-filtered results rather than the full dataset. This creates potentially confusing UX where:

  1. User sees "Active: 10" in the filter dropdown
  2. User selects "Active"
  3. Counts update to reflect only active records (e.g., "Active: 10, Inactive: 0")

This differs from Airtable's behavior where counts typically show the total available records for each option before filtering. Consider either:

  • Passing unfiltered data separately for count computation
  • Computing counts server-side
  • Documenting this behavior clearly if intentional

Copilot uses AI. Check for mistakes.
Comment on lines +233 to +242
<input
type="checkbox"
checked={selected.includes(opt.value)}
onChange={() => {
const next = selected.includes(opt.value)
? selected.filter(v => v !== opt.value)
: [...selected, opt.value];
handleChange(f.field, next);
}}
className="rounded border-input"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The checkbox input should have an aria-label or aria-labelledby attribute for better accessibility. While the wrapping <label> provides some context visually, screen readers may not properly associate the checkbox with its label text since the label content includes multiple elements (color dot, text, count).

Consider adding:

<input
  type="checkbox"
  aria-label={opt.label}
  checked={selected.includes(opt.value)}
  onChange={() => { /* ... */ }}
  className="rounded border-input"
/>

This ensures screen readers announce the option name when focusing on the checkbox.

Copilot uses AI. Check for mistakes.
Comment on lines +312 to +330
return (
<div className={cn('flex items-center gap-0.5 overflow-x-auto', className)} data-testid="user-filters-tabs">
{allTabs.map(tab => (
<button
key={tab.id}
data-testid={`filter-tab-${tab.id}`}
onClick={() => handleTabChange(tab.id)}
className={cn(
'inline-flex items-center h-7 px-3 text-xs font-medium rounded-md transition-colors shrink-0',
activeTab === tab.id
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-muted',
)}
>
{tab.label}
</button>
))}
{allowAddTab && (
<button
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The tab buttons should use proper ARIA tab semantics for better accessibility. Currently they're plain buttons, but they should follow the WAI-ARIA tabs pattern:

  1. Wrap in a <div role="tablist">
  2. Add role="tab", aria-selected, and aria-controls to each button
  3. Support keyboard navigation (Arrow keys, Home, End)

Alternatively, if these are meant to be toggle buttons rather than true tabs, consider using aria-pressed to indicate the active state.

This ensures screen reader users understand the component's purpose and can navigate it efficiently.

Suggested change
return (
<div className={cn('flex items-center gap-0.5 overflow-x-auto', className)} data-testid="user-filters-tabs">
{allTabs.map(tab => (
<button
key={tab.id}
data-testid={`filter-tab-${tab.id}`}
onClick={() => handleTabChange(tab.id)}
className={cn(
'inline-flex items-center h-7 px-3 text-xs font-medium rounded-md transition-colors shrink-0',
activeTab === tab.id
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-muted',
)}
>
{tab.label}
</button>
))}
{allowAddTab && (
<button
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!allTabs.length) return;
const currentIndex = allTabs.findIndex(tab => tab.id === activeTab);
const lastIndex = allTabs.length - 1;
let nextIndex = currentIndex === -1 ? 0 : currentIndex;
switch (event.key) {
case 'ArrowRight':
case 'ArrowDown': {
event.preventDefault();
nextIndex = currentIndex === -1 || currentIndex === lastIndex ? 0 : currentIndex + 1;
break;
}
case 'ArrowLeft':
case 'ArrowUp': {
event.preventDefault();
nextIndex = currentIndex <= 0 ? lastIndex : currentIndex - 1;
break;
}
case 'Home': {
event.preventDefault();
nextIndex = 0;
break;
}
case 'End': {
event.preventDefault();
nextIndex = lastIndex;
break;
}
default:
return;
}
const nextTab = allTabs[nextIndex];
if (!nextTab) return;
handleTabChange(nextTab.id);
const nextButton = document.getElementById(`filter-tab-${nextTab.id}`);
if (nextButton && typeof (nextButton as HTMLElement).focus === 'function') {
(nextButton as HTMLElement).focus();
}
},
[allTabs, activeTab, handleTabChange],
);
return (
<div className={cn('flex items-center gap-0.5 overflow-x-auto', className)} data-testid="user-filters-tabs">
<div role="tablist" aria-label="Filter tabs" onKeyDown={handleKeyDown} className="flex gap-0.5">
{allTabs.map(tab => (
<button
key={tab.id}
id={`filter-tab-${tab.id}`}
type="button"
role="tab"
aria-selected={activeTab === tab.id}
aria-controls={`${tab.id}-panel`}
tabIndex={activeTab === tab.id ? 0 : -1}
data-testid={`filter-tab-${tab.id}`}
onClick={() => handleTabChange(tab.id)}
className={cn(
'inline-flex items-center h-7 px-3 text-xs font-medium rounded-md transition-colors shrink-0',
activeTab === tab.id
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-muted',
)}
>
{tab.label}
</button>
))}
</div>
{allowAddTab && (
<button
type="button"

Copilot uses AI. Check for mistakes.
<button
className="inline-flex items-center justify-center h-7 w-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted shrink-0"
data-testid="filter-tab-add"
title="Add filter tab"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The add tab button should have an aria-label in addition to the title attribute. Screen readers often don't announce title attributes consistently. Add:

<button
  aria-label="Add filter tab"
  title="Add filter tab"
  className="..."
  data-testid="filter-tab-add"
>
  <Plus className="h-3.5 w-3.5" />
</button>

Also note that this button currently has no onClick handler, so it appears to be a placeholder for future functionality.

Suggested change
title="Add filter tab"
title="Add filter tab"
aria-label="Add filter tab"

Copilot uses AI. Check for mistakes.
Comment on lines +321 to 327
// Merge base filters, user filters, quick filters, and user filter bar conditions
const allFilters = [
...(baseFilter.length > 0 ? [baseFilter] : []),
...(userFilter.length > 0 ? [userFilter] : []),
...quickFilterConditions,
...userFilterConditions,
];
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The comment on line 321 could be more descriptive about what each filter source represents:

  • baseFilter (schema.filters): Static filters defined in the schema
  • userFilter (currentFilters): Filters from the FilterBuilder UI (advanced filter panel)
  • quickFilterConditions: Filters from toggle-style quick filter buttons
  • userFilterConditions: NEW - Filters from Airtable-style user filter bar (dropdown/tabs/toggle)

Consider updating the comment to clarify these distinctions, especially since userFilters is a new addition. For example:

// Merge all filter sources:
// 1. Base filters (schema.filters) - static schema-level filters
// 2. FilterBuilder (currentFilters) - advanced filter UI
// 3. Quick filters (schema.quickFilters) - toggle buttons
// 4. User filters (schema.userFilters) - Airtable-style dropdown/tabs/toggle

This helps future developers understand the complete filter pipeline.

Copilot uses AI. Check for mistakes.
field: string;
/** Display label (defaults to field label from objectDef) */
label?: string;
/** Filter input type */
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The type field defined in the schema (line 1228 in objectql.ts and line 211 in objectql.zod.ts) is not used in the UserFilters implementation. The field accepts values like 'select', 'multi-select', 'boolean', 'date-range', 'text', but the current implementation always treats filters as multi-select (using the 'in' operator).

If this field is reserved for future use, consider adding a comment explaining this. Otherwise, the implementation should use this field to determine:

  • Filter operator ('=' for single select vs 'in' for multi-select)
  • Input UI (checkbox vs radio buttons)
  • Value validation
Suggested change
/** Filter input type */
/**
* Filter input type (UI and behavior hint).
*
* This value is intended to guide:
* - Which operator is used (e.g. '=' for 'select' / 'boolean', 'in' for 'multi-select')
* - Which input widget is rendered (e.g. radios vs checkboxes vs text input)
* - Basic value validation (e.g. date range vs free text)
*
* Note: Current implementations of UserFilters MAY still treat all filters as
* multi-select (using the 'in' operator) and ignore this field. It is kept in
* the schema for forward compatibility so renderers/engines can progressively
* adopt richer, type-aware behavior without breaking existing views.
*/

Copilot uses AI. Check for mistakes.
Comment on lines +194 to +202
<button
data-testid={`filter-badge-${f.field}`}
className={cn(
'inline-flex items-center gap-1 rounded-md border h-7 px-2.5 text-xs font-medium transition-colors shrink-0',
hasSelection
? 'border-primary/30 bg-primary/5 text-primary'
: 'border-border bg-background hover:bg-accent text-foreground',
)}
>
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The filter badge button lacks proper accessibility attributes. Screen reader users won't know what the button does or what field it filters. Add an aria-label that describes the button's purpose, for example:

<button
  aria-label={`Filter by ${f.label || f.field}`}
  data-testid={`filter-badge-${f.field}`}
  // ... rest of props
>

This ensures the button is properly announced to assistive technologies.

Copilot uses AI. Check for mistakes.
Comment on lines +210 to +217
<X
className="h-3 w-3 opacity-60"
data-testid={`filter-clear-${f.field}`}
onClick={e => {
e.stopPropagation();
handleChange(f.field, []);
}}
/>
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The clear filter X icon lacks an aria-label, making it inaccessible to screen reader users. They won't know what clicking this icon does. Add an aria-label:

<X
  className="h-3 w-3 opacity-60"
  aria-label={`Clear ${f.label || f.field} filter`}
  data-testid={`filter-clear-${f.field}`}
  onClick={e => {
    e.stopPropagation();
    handleChange(f.field, []);
  }}
/>

Note: Since the X is rendered inside a button, you may also need to ensure the click handler doesn't interfere with the parent button's Popover trigger behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +972 to +982
{/* User Filters Row (Airtable Interfaces-style) */}
{schema.userFilters && (
<div className="border-b px-2 sm:px-4 py-1 bg-background" data-testid="user-filters">
<UserFilters
config={schema.userFilters}
objectDef={objectDef}
data={data}
onFilterChange={setUserFilterConditions}
/>
</div>
)}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The userFilters feature lacks integration tests with ListView. While UserFilters.test.tsx comprehensively tests the component in isolation, there are no tests verifying:

  1. The filters are properly rendered in the ListView toolbar
  2. Filter changes trigger data refetching with correct filter conditions
  3. userFilters work alongside existing quickFilters and FilterBuilder
  4. The data dependency (passing filtered data for counts) works correctly

Consider adding integration tests in ListView.test.tsx similar to the existing quickFilters tests (line 265-282), such as:

it('should render user filters when configured', () => {
  const schema: ListViewSchema = {
    type: 'list-view',
    objectName: 'contacts',
    userFilters: {
      element: 'dropdown',
      fields: [{ field: 'status', label: 'Status' }]
    }
  };
  renderWithProvider(<ListView schema={schema} />);
  expect(screen.getByTestId('user-filters')).toBeInTheDocument();
});

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🎯 Feature: Airtable Interfaces 风格 User Filters — 列表视图顶部过滤器(Dropdown / Tabs / Toggle 三模式)

3 participants