Skip to content

Commit 8c16f62

Browse files
authored
Merge pull request #1042 from objectstack-ai/copilot/optimize-lookup-field-popup
2 parents 90aa0ff + 6c3a801 commit 8c16f62

File tree

5 files changed

+844
-99
lines changed

5 files changed

+844
-99
lines changed

ROADMAP.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
131131
- [ ] Advanced lookup: dependent lookups (filter based on other fields)
132132
- [ ] Hierarchical lookups (parent-child relationships)
133133
- [ ] Lookup result caching
134+
- [x] Lookup field dynamic DataSource loading — popup fetches records via `DataSource.find()` with `$search` debounce, loading/error/empty states
135+
- [x] Lookup field context DataSource — reads DataSource from SchemaRendererContext so forms work without explicit prop
136+
- [x] Lookup field UX polish — arrow key navigation, description field display, quick-create entry, ARIA listbox roles
134137
- [ ] Form conditional logic with branching
135138
- [ ] Multi-page forms with progress indicator
136139

content/docs/fields/lookup.mdx

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: "Reference field for linking to other objects/records"
55

66
import { ComponentDemo, DemoGrid } from '@/app/components/ComponentDemo';
77

8-
The Lookup Field component provides a reference field for creating relationships between objects and records.
8+
The Lookup Field component provides a reference field for creating relationships between objects and records. It supports dynamic data loading from a DataSource with debounced search, loading/error/empty states, keyboard navigation, and optional quick-create entry.
99

1010
## Basic Usage
1111

@@ -62,24 +62,51 @@ interface LookupFieldSchema {
6262
// Reference Configuration
6363
reference_to: string; // Referenced object/table name
6464
reference_field?: string; // Field to display (default: 'name')
65+
description_field?: string; // Secondary field shown below label
66+
id_field?: string; // ID field on records (default: '_id')
6567
options?: LookupOption[]; // Available options (if static)
6668
67-
// Data Source (for dynamic lookups)
68-
dataSource?: {
69-
api?: string; // API endpoint
70-
method?: string; // HTTP method
71-
params?: Record<string, any>; // Query parameters
72-
};
69+
// Data Source (automatic via SchemaRendererContext, or explicit)
70+
// When a DataSource is available, the popup dynamically loads
71+
// records from the referenced object on open, with debounced search.
72+
dataSource?: DataSource;
73+
74+
// Quick-create callback (shown when no results found)
75+
onCreateNew?: (searchQuery: string) => void;
7376
}
7477
7578
interface LookupOption {
7679
label: string; // Display label
7780
value: string; // Record ID
81+
description?: string; // Secondary text below label
7882
_id?: string; // Alternative ID field
7983
name?: string; // Alternative label field
8084
}
8185
```
8286

87+
## Dynamic Data Source
88+
89+
When a `DataSource` is available (via `SchemaRendererContext`, explicit prop, or field config), the Lookup popup **automatically** fetches records from the referenced object:
90+
91+
```plaintext
92+
// Automatic — DataSource from SchemaRendererContext
93+
// (works out-of-the-box in ObjectForm, DrawerForm, etc.)
94+
{
95+
type: 'lookup',
96+
name: 'customer',
97+
label: 'Customer',
98+
reference_to: 'customers',
99+
reference_field: 'name', // Display field (default: 'name')
100+
description_field: 'industry', // Optional secondary field
101+
}
102+
```
103+
104+
The popup will:
105+
1. Fetch records via `dataSource.find(reference_to, { $top: 50 })` on open
106+
2. Send `$search` queries with 300ms debounce as the user types
107+
3. Show loading spinner, error state with retry, and empty state
108+
4. Display "Showing X of Y" when more records exist than the page size
109+
83110
## Lookup vs Master-Detail
84111

85112
- **Lookup**: Standard reference field, can be deleted independently
@@ -104,31 +131,14 @@ import { LookupCellRenderer } from '@object-ui/fields';
104131
- **Parent Records**: Master-detail relationships
105132
- **Team Members**: Multi-user references
106133

107-
## Dynamic Data Source
108-
109-
For lookups that fetch data from an API:
110-
111-
```plaintext
112-
{
113-
type: 'lookup',
114-
name: 'customer',
115-
label: 'Customer',
116-
reference_to: 'customers',
117-
dataSource: {
118-
api: '/api/customers',
119-
method: 'GET',
120-
params: {
121-
active: true,
122-
limit: 100
123-
}
124-
}
125-
}
126-
```
127-
128134
## Features
129135

130-
- **Search**: Type-ahead search in options
136+
- **Dynamic DataSource Loading**: Automatically fetches records from referenced objects
137+
- **Search**: Debounced type-ahead search with `$search` parameter
131138
- **Multi-Select**: Support for multiple references
132-
- **Lazy Loading**: Load options on demand
133-
- **Relationships**: Create data relationships
134-
- **Cascading**: Support for dependent lookups
139+
- **Keyboard Navigation**: Arrow keys to navigate, Enter to select
140+
- **Loading/Error/Empty States**: Friendly feedback for all states
141+
- **Secondary Field Display**: Show description/subtitle per option
142+
- **Quick-Create Entry**: Optional "Create new" button when no results
143+
- **Pagination Hint**: Shows total count when more results available
144+
- **Backward Compatible**: Falls back to static options when no DataSource

packages/components/src/renderers/form/form.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { Alert, AlertDescription } from '../../ui/alert';
2727
import { AlertCircle, Loader2 } from 'lucide-react';
2828
import { cn } from '../../lib/utils';
2929
import React from 'react';
30+
import { SchemaRendererContext } from '@object-ui/react';
3031

3132
// Form renderer component - Airtable-style feature-complete form
3233
ComponentRegistry.register('form',
@@ -57,6 +58,11 @@ ComponentRegistry.register('form',
5758
const [isSubmitting, setIsSubmitting] = React.useState(false);
5859
const [submitError, setSubmitError] = React.useState<string | null>(null);
5960

61+
// Read DataSource from SchemaRendererContext and propagate it to field
62+
// widgets as a prop so they can dynamically load related records.
63+
const schemaCtx = React.useContext(SchemaRendererContext);
64+
const contextDataSource = schemaCtx?.dataSource ?? null;
65+
6066
// React to defaultValues changes
6167
React.useEffect(() => {
6268
form.reset(defaultValues);
@@ -313,6 +319,7 @@ ComponentRegistry.register('form',
313319
options: fieldProps.options,
314320
placeholder: fieldProps.placeholder,
315321
disabled: disabled || fieldDisabled || readonly || isSubmitting,
322+
dataSource: contextDataSource,
316323
})}
317324
</FormControl>
318325
{description && (

0 commit comments

Comments
 (0)