diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a19c6575..6fad0b5e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **"Browse All" Button for Lookup Fields** (`@object-ui/fields`): Added an always-visible "Browse All" (table icon) button next to the Lookup quick-select trigger. Opens the full RecordPickerDialog directly, regardless of record count — making enterprise features (multi-column table, sort/filter bar, cell renderers) discoverable at all times. Previously, the dialog was only accessible via the "Show All Results" in-popover button, which only appeared when total records exceeded the page size. The button uses accessible `aria-label`, `title`, and Lucide `TableProperties` icon. Keyboard and screen reader accessible. - **CRM Enterprise Lookup Metadata** (`examples/crm`): All 14 lookup fields across 8 CRM objects now have enterprise-grade RecordPicker configuration — `lookup_columns` (with type hints for cell rendering: select, currency, boolean, date, number, percent), `lookup_filters` (base business filters using eq/ne/in/notIn operators), and `description_field`. Uses post-create `Object.assign` injection pattern to bypass `ObjectSchema.create()` Zod stripping (analogous to the listViews passthrough approach). - **Enterprise Lookup Tests** (`examples/crm`): 12 new test cases validating lookup_columns presence & type diversity, lookup_filters operator validity, description_field coverage, and specific business logic (e.g., active-only users, non-cancelled orders, open opportunities). - **RecordPickerDialog Component** (`@object-ui/fields`): New enterprise-grade record selection dialog with multi-column table display, pagination, search, column sorting with `$orderby`, keyboard navigation (Arrow keys + Enter), loading/error/empty states, and single/multi-select support. Responsive layout with mobile-friendly width. Provides the foundation for Salesforce-style Lookup experience. diff --git a/content/docs/fields/lookup.mdx b/content/docs/fields/lookup.mdx index 1e26b1cda..27d3730b9 100644 --- a/content/docs/fields/lookup.mdx +++ b/content/docs/fields/lookup.mdx @@ -116,11 +116,21 @@ The popup will: 2. Send `$search` queries with 300ms debounce as the user types 3. Show loading spinner, error state with retry, and empty state 4. Display "Showing X of Y" when more records exist than the page size -5. Show a **"Show All Results"** button to open the full Record Picker dialog +5. Show a **"Show All Results"** button (inside the popover) to open the full Record Picker dialog when total exceeds page size + +## Browse All Button + +Every Lookup field with a `dataSource` always renders a **"Browse All"** button (table icon) next to the quick-select trigger. This button opens the full **RecordPickerDialog** directly, regardless of dataset size — ensuring enterprise features like multi-column tables, sort/filter bar, and cell renderers are always discoverable. + +- Always visible when `dataSource` is configured +- Opens the Record Picker dialog without needing to open the popover first +- Keyboard accessible and screen-reader friendly (`aria-label="Browse all records"`) ## Record Picker Dialog (Enterprise) -When more results are available than displayed in the quick-select popup, a **"Show All Results"** button opens the full **RecordPickerDialog** — an enterprise-grade record selection experience. +The full **RecordPickerDialog** can be opened in two ways: +1. **"Browse All" button** (table icon) — always visible next to the quick-select trigger +2. **"Show All Results"** link inside the popover — shown when total records exceed the page size ```plaintext // Configure the Record Picker with lookup_columns diff --git a/packages/fields/src/record-picker.test.tsx b/packages/fields/src/record-picker.test.tsx index 9f9d21486..d7ac31a61 100644 --- a/packages/fields/src/record-picker.test.tsx +++ b/packages/fields/src/record-picker.test.tsx @@ -439,6 +439,77 @@ describe('LookupField — Show All Results', () => { }); }); +// ------------- LookupField — Browse All Button (Always Visible) ------------- + +describe('LookupField — Browse All Button', () => { + const mockField = { + name: 'customer', + label: 'Customer', + reference_to: 'customers', + reference_field: 'name', + } as any; + + const baseProps: FieldWidgetProps = { + field: mockField, + value: undefined, + onChange: vi.fn(), + readonly: false, + dataSource: mockDataSource as any, + }; + + it('renders "Browse All" button when dataSource is available, even with <5 records', async () => { + mockDataSource.find.mockResolvedValue({ + data: [ + { id: '1', name: 'Alpha' }, + { id: '2', name: 'Beta' }, + { id: '3', name: 'Gamma' }, + ], + total: 3, + }); + + render(); + + // "Browse All" button should always be visible (not inside popover) + expect(screen.getByTestId('browse-all-records')).toBeInTheDocument(); + expect(screen.getByLabelText('Browse all records')).toBeInTheDocument(); + }); + + it('opens RecordPickerDialog when "Browse All" is clicked with small dataset', async () => { + mockDataSource.find.mockResolvedValue({ + data: [ + { id: '1', name: 'Alpha' }, + { id: '2', name: 'Beta' }, + ], + total: 2, + }); + + render(); + + // Click "Browse All" button directly (no need to open popover first) + await act(async () => { + fireEvent.click(screen.getByTestId('browse-all-records')); + }); + + // RecordPickerDialog should now be open + await waitFor(() => { + expect(screen.getByTestId('record-picker-dialog')).toBeInTheDocument(); + }); + }); + + it('does not render "Browse All" button when no dataSource is available', () => { + const propsWithoutDS: FieldWidgetProps = { + field: { ...mockField, options: [{ value: '1', label: 'Opt 1' }] } as any, + value: undefined, + onChange: vi.fn(), + readonly: false, + }; + + render(); + + expect(screen.queryByTestId('browse-all-records')).not.toBeInTheDocument(); + }); +}); + // ------------- RecordPickerDialog — Column Sorting ------------- describe('RecordPickerDialog — Column Sorting', () => { diff --git a/packages/fields/src/widgets/LookupField.tsx b/packages/fields/src/widgets/LookupField.tsx index b14192f86..113d29dfb 100644 --- a/packages/fields/src/widgets/LookupField.tsx +++ b/packages/fields/src/widgets/LookupField.tsx @@ -377,11 +377,12 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel )} {/* Level 1: Quick-select Popover (inline typeahead) */} +
+ )} +
+ {/* Level 2: Full Record Picker Dialog */} {hasDataSource && dataSource && referenceTo && (