Skip to content

Commit 8be85db

Browse files
authored
Merge pull request #1092 from objectstack-ai/copilot/add-browse-all-button-lookup
2 parents 9404248 + cb0334e commit 8be85db

File tree

4 files changed

+103
-3
lines changed

4 files changed

+103
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **"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.
1213
- **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).
1314
- **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).
1415
- **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.

content/docs/fields/lookup.mdx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,21 @@ The popup will:
116116
2. Send `$search` queries with 300ms debounce as the user types
117117
3. Show loading spinner, error state with retry, and empty state
118118
4. Display "Showing X of Y" when more records exist than the page size
119-
5. Show a **"Show All Results"** button to open the full Record Picker dialog
119+
5. Show a **"Show All Results"** button (inside the popover) to open the full Record Picker dialog when total exceeds page size
120+
121+
## Browse All Button
122+
123+
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.
124+
125+
- Always visible when `dataSource` is configured
126+
- Opens the Record Picker dialog without needing to open the popover first
127+
- Keyboard accessible and screen-reader friendly (`aria-label="Browse all records"`)
120128

121129
## Record Picker Dialog (Enterprise)
122130

123-
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.
131+
The full **RecordPickerDialog** can be opened in two ways:
132+
1. **"Browse All" button** (table icon) — always visible next to the quick-select trigger
133+
2. **"Show All Results"** link inside the popover — shown when total records exceed the page size
124134

125135
```plaintext
126136
// Configure the Record Picker with lookup_columns

packages/fields/src/record-picker.test.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,77 @@ describe('LookupField — Show All Results', () => {
439439
});
440440
});
441441

442+
// ------------- LookupField — Browse All Button (Always Visible) -------------
443+
444+
describe('LookupField — Browse All Button', () => {
445+
const mockField = {
446+
name: 'customer',
447+
label: 'Customer',
448+
reference_to: 'customers',
449+
reference_field: 'name',
450+
} as any;
451+
452+
const baseProps: FieldWidgetProps<any> = {
453+
field: mockField,
454+
value: undefined,
455+
onChange: vi.fn(),
456+
readonly: false,
457+
dataSource: mockDataSource as any,
458+
};
459+
460+
it('renders "Browse All" button when dataSource is available, even with <5 records', async () => {
461+
mockDataSource.find.mockResolvedValue({
462+
data: [
463+
{ id: '1', name: 'Alpha' },
464+
{ id: '2', name: 'Beta' },
465+
{ id: '3', name: 'Gamma' },
466+
],
467+
total: 3,
468+
});
469+
470+
render(<LookupField {...baseProps} />);
471+
472+
// "Browse All" button should always be visible (not inside popover)
473+
expect(screen.getByTestId('browse-all-records')).toBeInTheDocument();
474+
expect(screen.getByLabelText('Browse all records')).toBeInTheDocument();
475+
});
476+
477+
it('opens RecordPickerDialog when "Browse All" is clicked with small dataset', async () => {
478+
mockDataSource.find.mockResolvedValue({
479+
data: [
480+
{ id: '1', name: 'Alpha' },
481+
{ id: '2', name: 'Beta' },
482+
],
483+
total: 2,
484+
});
485+
486+
render(<LookupField {...baseProps} />);
487+
488+
// Click "Browse All" button directly (no need to open popover first)
489+
await act(async () => {
490+
fireEvent.click(screen.getByTestId('browse-all-records'));
491+
});
492+
493+
// RecordPickerDialog should now be open
494+
await waitFor(() => {
495+
expect(screen.getByTestId('record-picker-dialog')).toBeInTheDocument();
496+
});
497+
});
498+
499+
it('does not render "Browse All" button when no dataSource is available', () => {
500+
const propsWithoutDS: FieldWidgetProps<any> = {
501+
field: { ...mockField, options: [{ value: '1', label: 'Opt 1' }] } as any,
502+
value: undefined,
503+
onChange: vi.fn(),
504+
readonly: false,
505+
};
506+
507+
render(<LookupField {...propsWithoutDS} />);
508+
509+
expect(screen.queryByTestId('browse-all-records')).not.toBeInTheDocument();
510+
});
511+
});
512+
442513
// ------------- RecordPickerDialog — Column Sorting -------------
443514

444515
describe('RecordPickerDialog — Column Sorting', () => {

packages/fields/src/widgets/LookupField.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,12 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
377377
)}
378378

379379
{/* Level 1: Quick-select Popover (inline typeahead) */}
380+
<div className="flex items-center gap-1.5">
380381
<Popover open={isOpen} onOpenChange={setIsOpen}>
381382
<PopoverTrigger asChild>
382383
<Button
383384
variant="outline"
384-
className="w-full justify-start text-left font-normal"
385+
className="min-w-0 flex-1 justify-start text-left font-normal"
385386
type="button"
386387
>
387388
<Search className="mr-2 size-4" />
@@ -541,6 +542,23 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
541542
</PopoverContent>
542543
</Popover>
543544

545+
{/* "Browse All" button — always visible when DataSource is available */}
546+
{hasDataSource && (
547+
<Button
548+
variant="outline"
549+
size="icon"
550+
className="shrink-0"
551+
type="button"
552+
onClick={() => setIsPickerOpen(true)}
553+
aria-label="Browse all records"
554+
title="Browse all records"
555+
data-testid="browse-all-records"
556+
>
557+
<TableProperties className="size-4" />
558+
</Button>
559+
)}
560+
</div>
561+
544562
{/* Level 2: Full Record Picker Dialog */}
545563
{hasDataSource && dataSource && referenceTo && (
546564
<RecordPickerDialog

0 commit comments

Comments
 (0)