diff --git a/.changeset/silent-maps-kick.md b/.changeset/silent-maps-kick.md new file mode 100644 index 00000000..cd526312 --- /dev/null +++ b/.changeset/silent-maps-kick.md @@ -0,0 +1,5 @@ +--- +"@llamaindex/ui": patch +--- + +Add filter/styles to grid diff --git a/packages/ui/src/item-grid/extracted-data-item-grid.tsx b/packages/ui/src/item-grid/extracted-data-item-grid.tsx index dc8629a4..af67ed91 100644 --- a/packages/ui/src/item-grid/extracted-data-item-grid.tsx +++ b/packages/ui/src/item-grid/extracted-data-item-grid.tsx @@ -1,6 +1,7 @@ import type { TypedAgentData, ExtractedData, + FilterOperation, } from "llama-cloud-services/beta/agent"; import { ItemGrid } from "./item-grid"; import { @@ -19,6 +20,11 @@ export interface ExtractedDataItemGridProps { onRowClick?: (item: TypedAgentData>) => void; // Other configurations defaultPageSize?: number; + // Optional base filter to be passed to search API + filter?: Record; + // Styling (forwarded to ItemGrid) + className?: string; + style?: React.CSSProperties; } export function ExtractedDataItemGrid({ @@ -26,6 +32,9 @@ export function ExtractedDataItemGrid({ builtInColumns = {}, onRowClick, defaultPageSize = 20, + filter, + className, + style, }: ExtractedDataItemGridProps) { const confidenceThreshold = useUIConfigStore( (state) => state.confidenceThreshold @@ -58,6 +67,9 @@ export function ExtractedDataItemGrid({ customColumns={columns} onRowClick={onRowClick} defaultPageSize={defaultPageSize} + filter={filter} + className={className} + style={style} /> ); } diff --git a/packages/ui/src/item-grid/item-grid.tsx b/packages/ui/src/item-grid/item-grid.tsx index 89a27839..87bdca69 100644 --- a/packages/ui/src/item-grid/item-grid.tsx +++ b/packages/ui/src/item-grid/item-grid.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, type CSSProperties } from "react"; import { cn } from "@/lib/utils"; import { Table, @@ -35,6 +35,11 @@ export interface ItemGridProp { onRowClick?: (item: TypedAgentData) => void; // Other configurations defaultPageSize?: number; + // Optional root filter passed through directly to the search API + filter?: Record; + // Styling (outermost container only) + className?: string; + style?: CSSProperties; } // Main Business Component @@ -42,6 +47,9 @@ export function ItemGrid({ customColumns = [], onRowClick, defaultPageSize = 20, + filter, + className, + style, }: ItemGridProp) { const [paginationState, setPaginationState] = useState({ page: 0, @@ -54,7 +62,7 @@ export function ItemGrid({ direction: "desc", }); - const [filters, setFilters] = useState>({}); + const [uiFilters, setUiFilters] = useState>({}); // Generate final columns array const columns = useMemo(() => { @@ -67,17 +75,19 @@ export function ItemGrid({ }, [customColumns]); // Convert frontend filter state to API format - const apiFilters = useMemo(() => { - const result: Record = {}; + const searchFilter = useMemo(() => { + const result: Record = { + ...(filter || {}), + }; - Object.entries(filters).forEach(([columnKey, filterValues]) => { + Object.entries(uiFilters).forEach(([columnKey, filterValues]) => { if (filterValues.length > 0) { result[columnKey] = { includes: filterValues }; } }); return result; - }, [filters]); + }, [uiFilters, filter]); // Convert frontend sort state to API format const apiSort = useMemo(() => { @@ -91,7 +101,7 @@ export function ItemGrid({ }, [sortState]); const { data, loading, error, totalSize, deleteItem, fetchData } = - useItemGridData(paginationState, apiFilters, apiSort); + useItemGridData(paginationState, searchFilter, apiSort); // Create hooks object for passing to renderCell const hooks = useMemo( @@ -122,7 +132,7 @@ export function ItemGrid({ // Handle filtering const handleFilterChange = (columnKey: string, values: string[]) => { - setFilters((prev) => ({ + setUiFilters((prev) => ({ ...prev, [columnKey]: values, })); @@ -168,7 +178,7 @@ export function ItemGrid({ } return ( -
+
@@ -186,7 +196,7 @@ export function ItemGrid({ sortState={sortState} onSort={handleSort} filterOptions={getFilterOptions(column.key)} - selectedFilters={filters[column.key]} + selectedFilters={uiFilters[column.key]} onFilterChange={handleFilterChange} /> diff --git a/packages/ui/tests/item-grid/extracted-data-item-grid.filter.test.tsx b/packages/ui/tests/item-grid/extracted-data-item-grid.filter.test.tsx new file mode 100644 index 00000000..60fdf106 --- /dev/null +++ b/packages/ui/tests/item-grid/extracted-data-item-grid.filter.test.tsx @@ -0,0 +1,41 @@ +import { describe, it, expect, vi } from "vitest"; +import { render } from "@testing-library/react"; +import { ApiProvider, createMockClients } from "../../src/lib"; +import { ExtractedDataItemGrid } from "../../src/item-grid/extracted-data-item-grid"; +import type { FilterOperation } from "llama-cloud-services/beta/agent"; +import type { ReactNode } from "react"; + +function renderWithProvider(ui: ReactNode, clients = createMockClients()) { + return render({ui}); +} + +describe("ExtractedDataItemGrid baseFilter", () => { + it("passes filter through to search API", async () => { + const clients = createMockClients(); + if (!clients.agentDataClient) { + throw new Error("AgentDataClient not found"); + } + const spy = vi.spyOn(clients.agentDataClient, "search"); + + const filter: Record = { + status: { includes: ["approved"] }, + }; + + renderWithProvider( + , + clients + ); + + // Wait a microtask tick for effects to run + await Promise.resolve(); + + expect(spy).toHaveBeenCalled(); + const call = spy.mock.calls.at(-1) as any[]; + expect(call?.[0]?.filter).toMatchObject(filter); + }); +});