Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 12 additions & 2 deletions content/docs/fields/lookup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions packages/fields/src/record-picker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> = {
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(<LookupField {...baseProps} />);

// "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(<LookupField {...baseProps} />);

// 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<any> = {
field: { ...mockField, options: [{ value: '1', label: 'Opt 1' }] } as any,
value: undefined,
onChange: vi.fn(),
readonly: false,
};

render(<LookupField {...propsWithoutDS} />);

expect(screen.queryByTestId('browse-all-records')).not.toBeInTheDocument();
});
});

// ------------- RecordPickerDialog — Column Sorting -------------

describe('RecordPickerDialog — Column Sorting', () => {
Expand Down
20 changes: 19 additions & 1 deletion packages/fields/src/widgets/LookupField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,11 +377,12 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
)}

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

{/* "Browse All" button — always visible when DataSource is available */}
{hasDataSource && (
<Button
variant="outline"
size="icon"
className="shrink-0"
type="button"
onClick={() => setIsPickerOpen(true)}
aria-label="Browse all records"
title="Browse all records"
data-testid="browse-all-records"
>
<TableProperties className="size-4" />
</Button>
)}
</div>

{/* Level 2: Full Record Picker Dialog */}
{hasDataSource && dataSource && referenceTo && (
<RecordPickerDialog
Expand Down
Loading