diff --git a/ROADMAP.md b/ROADMAP.md index aecb625b..134ad4f9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -932,6 +932,10 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th - [x] Separators conditionally rendered only when adjacent groups are visible - [x] Inline search moved to toolbar left end (`w-48`, Airtable-style) - [x] Density button: activated state highlight (`bg-primary/10 border border-primary/20`) when density is non-default +- [x] Merged UserFilters row and tool buttons row into single toolbar line — left: field filter badges, right: tool buttons with separator +- [x] Search changed from inline input to icon button + Popover — saves toolbar space, matches Airtable pattern +- [x] UserFilters `maxVisible` prop added — overflow badges collapse into "More" dropdown with Popover +- [x] Toolbar layout uses flex with `min-w-0` overflow handling for responsive behavior **Platform: ViewTabBar:** - [x] Tab "•" dot indicator replaced with descriptive badge (`F`/`S`/`FS`) + tooltip showing "Active filters", "Active sort" @@ -956,7 +960,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th **Tests:** - [x] 7 new CRM metadata tests validating column types, widths, rowHeight, conditionalFormatting - [x] 136 ViewConfigPanel tests updated for defaultCollapsed sections (expand before access) -- [x] 411 ListView + ViewTabBar tests passing +- [x] 411 ListView + ViewTabBar tests passing (255 plugin-list tests including 9 new toolbar/collapse tests) - [x] 11 AppSidebar tests passing ### P2.5 PWA & Offline (Real Sync) diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index 287cab70..0b492b73 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -287,6 +287,7 @@ export const ListView: React.FC = ({ (schema.viewType as ViewType) ); const [searchTerm, setSearchTerm] = React.useState(''); + const [showSearchPopover, setShowSearchPopover] = React.useState(false); // Sort State const [showSort, setShowSort] = React.useState(false); @@ -1016,35 +1017,23 @@ export const ListView: React.FC = ({ )} - {/* Airtable-style Toolbar — Row 2: Tool buttons */} + {/* Airtable-style Toolbar — Merged: UserFilter badges (left) + Tool buttons (right) */}
- {/* Search (left end — Airtable-style) */} - {toolbarFlags.showSearch && ( -
- - handleSearchChange(e.target.value)} - className="pl-7 h-7 text-xs bg-muted/50 border-transparent hover:bg-muted focus:bg-background focus:border-input transition-colors" - /> - {searchTerm && ( - - )} -
- )} - - {/* --- Separator: Search | Fields --- */} - {toolbarFlags.showSearch && toolbarFlags.showHideFields && ( -
+ {/* User Filters — inline in toolbar (Airtable Interfaces-style) */} + {resolvedUserFilters && ( + <> +
+ +
+
+ )} {/* Hide Fields */} @@ -1387,6 +1376,56 @@ export const ListView: React.FC = ({ Print )} + + {/* --- Separator: Print/Share/Export | Search --- */} + {(() => { + const hasLeftSideItems = schema.allowPrinting || (schema.sharing?.enabled || schema.sharing?.type) || (resolvedExportOptions && schema.allowExport !== false); + return toolbarFlags.showSearch && hasLeftSideItems ? ( +
+ ) : null; + })()} + + {/* Search (icon button + popover) */} + {toolbarFlags.showSearch && ( + + + + + +
+ + handleSearchChange(e.target.value)} + className="pl-7 h-8 text-xs" + autoFocus + /> + {searchTerm && ( + + )} +
+
+
+ )}
{/* Right: Add Record */} @@ -1436,18 +1475,6 @@ export const ListView: React.FC = ({
)} - {/* User Filters Row (Airtable Interfaces-style) */} - {resolvedUserFilters && ( -
- -
- )} - {/* View Content */}
{!loading && data.length === 0 ? ( diff --git a/packages/plugin-list/src/UserFilters.tsx b/packages/plugin-list/src/UserFilters.tsx index 1f069c46..819bfb28 100644 --- a/packages/plugin-list/src/UserFilters.tsx +++ b/packages/plugin-list/src/UserFilters.tsx @@ -37,6 +37,8 @@ export interface UserFiltersProps { data?: any[]; /** Callback when filter state changes */ onFilterChange: (filters: any[]) => void; + /** Maximum visible filter badges before collapsing into "More" dropdown (dropdown mode only) */ + maxVisible?: number; className?: string; } @@ -53,6 +55,7 @@ export function UserFilters({ objectDef, data = [], onFilterChange, + maxVisible, className, }: UserFiltersProps) { switch (config.element) { @@ -63,6 +66,7 @@ export function UserFilters({ objectDef={objectDef} data={data} onFilterChange={onFilterChange} + maxVisible={maxVisible} className={className} /> ); @@ -138,10 +142,11 @@ interface DropdownFiltersProps { objectDef?: any; data: any[]; onFilterChange: (filters: any[]) => void; + maxVisible?: number; className?: string; } -function DropdownFilters({ fields, objectDef, data, onFilterChange, className }: DropdownFiltersProps) { +function DropdownFilters({ fields, objectDef, data, onFilterChange, maxVisible, className }: DropdownFiltersProps) { const [selectedValues, setSelectedValues] = React.useState< Record >(() => { @@ -182,6 +187,89 @@ function DropdownFilters({ fields, objectDef, data, onFilterChange, className }: // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Split fields into visible and overflow based on maxVisible + const visibleFields = maxVisible !== undefined && maxVisible < resolvedFields.length + ? resolvedFields.slice(0, maxVisible) + : resolvedFields; + const overflowFields = maxVisible !== undefined && maxVisible < resolvedFields.length + ? resolvedFields.slice(maxVisible) + : []; + + const renderBadge = (f: ResolvedField) => { + const selected = selectedValues[f.field] || []; + const hasSelection = selected.length > 0; + + return ( + + + + + +
+ {f.options.map(opt => ( + + ))} +
+
+
+ ); + }; + return (
@@ -190,80 +278,30 @@ function DropdownFilters({ fields, objectDef, data, onFilterChange, className }: No filter fields ) : ( - resolvedFields.map(f => { - const selected = selectedValues[f.field] || []; - const hasSelection = selected.length > 0; - - return ( - + <> + {visibleFields.map(renderBadge)} + {overflowFields.length > 0 && ( + - -
- {f.options.map(opt => ( - - ))} + +
+ {overflowFields.map(renderBadge)}
- ); - }) + )} + )}