@object-ui/fields 3.1.3 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/AddressField-B6fPb2Fs.js +3 -0
  2. package/dist/AddressField-Ctsxkj35.cjs +1 -0
  3. package/dist/AutoNumberField-DXIYJfiB.js +3 -0
  4. package/dist/AutoNumberField-f76XU1bR.cjs +1 -0
  5. package/dist/AvatarField-BHpex3Wa.js +3 -0
  6. package/dist/AvatarField-CXOdYavl.cjs +1 -0
  7. package/dist/BooleanField-Bs0urhsx.js +3 -0
  8. package/dist/BooleanField-DeexI_O2.cjs +1 -0
  9. package/dist/CodeField-DDOMH8E8.cjs +1 -0
  10. package/dist/CodeField-tRACqngO.js +3 -0
  11. package/dist/ColorField-Bar6bOJw.js +3 -0
  12. package/dist/ColorField-CM9CxZ9h.cjs +1 -0
  13. package/dist/CurrencyField-CezTX_rA.cjs +1 -0
  14. package/dist/CurrencyField-D4VwXI9k.js +3 -0
  15. package/dist/DateField-DDUllj7w.cjs +1 -0
  16. package/dist/DateField-DjGiK6BH.js +3 -0
  17. package/dist/DateTimeField-DBh-dYdR.js +3 -0
  18. package/dist/DateTimeField-DDqgVN3b.cjs +1 -0
  19. package/dist/EmailField-B-3nY8Pr.cjs +1 -0
  20. package/dist/EmailField-Cp6yJlpH.js +3 -0
  21. package/dist/FileField-C1bcL7tu.cjs +1 -0
  22. package/dist/FileField-DUgt6ai3.js +3 -0
  23. package/dist/FormulaField-BLV1KgkQ.js +3 -0
  24. package/dist/FormulaField-CsbQIZ7S.cjs +1 -0
  25. package/dist/GeolocationField-BbIRH51u.js +3 -0
  26. package/dist/GeolocationField-UFu9VR-x.cjs +1 -0
  27. package/dist/GridField-2m74haE0.cjs +1 -0
  28. package/dist/GridField-O3Jd1NuJ.js +3 -0
  29. package/dist/ImageField-Cna7g6KL.cjs +1 -0
  30. package/dist/ImageField-rc1Nzcvb.js +3 -0
  31. package/dist/LocationField-DpRe9QlH.cjs +1 -0
  32. package/dist/LocationField-QZPPYo0J.js +3 -0
  33. package/dist/LookupField-BX4sK8NG.js +904 -0
  34. package/dist/LookupField-C9rPrHi_.cjs +1 -0
  35. package/dist/MasterDetailField-BXncIT1s.js +3 -0
  36. package/dist/MasterDetailField-TNP2825Z.cjs +1 -0
  37. package/dist/NumberField-DCHTzmDG.js +3 -0
  38. package/dist/NumberField-SqRwbcY8.cjs +1 -0
  39. package/dist/ObjectField-BEUHpn_X.js +3 -0
  40. package/dist/ObjectField-BWZwaFIG.cjs +1 -0
  41. package/dist/PasswordField-BZ_OeQyw.js +3 -0
  42. package/dist/PasswordField-kLp-ZyPm.cjs +1 -0
  43. package/dist/PercentField-B2t2knlc.js +3 -0
  44. package/dist/PercentField-BzhD6tQm.cjs +1 -0
  45. package/dist/PhoneField-CirvuysP.cjs +1 -0
  46. package/dist/PhoneField-CkrCFmcO.js +3 -0
  47. package/dist/QRCodeField-C4OOLCbE.cjs +1 -0
  48. package/dist/QRCodeField-CQoGow8y.js +3 -0
  49. package/dist/RatingField-CZG8SgQ8.cjs +1 -0
  50. package/dist/RatingField-DecDJkou.js +3 -0
  51. package/dist/RichTextField-Bog2i4NN.cjs +1 -0
  52. package/dist/RichTextField-b-8QbNCJ.js +3 -0
  53. package/dist/SelectField-CAa6c0cB.cjs +1 -0
  54. package/dist/SelectField-DMy5IxRc.js +3 -0
  55. package/dist/SignatureField-BvTPE7Dn.js +3 -0
  56. package/dist/SignatureField-jsb-0AYp.cjs +1 -0
  57. package/dist/SliderField-B7WvKFv7.js +3 -0
  58. package/dist/SliderField-DRjEJSxK.cjs +1 -0
  59. package/dist/SummaryField-DA16Edq3.cjs +1 -0
  60. package/dist/SummaryField-DVSVfKmY.js +3 -0
  61. package/dist/TextAreaField--ut87UiB.js +3 -0
  62. package/dist/TextAreaField-Cxp3DFC6.cjs +1 -0
  63. package/dist/TextField-CkEDGbkQ.js +3 -0
  64. package/dist/TextField-aLAzBaLZ.cjs +1 -0
  65. package/dist/TimeField-D0lF0y_I.cjs +1 -0
  66. package/dist/TimeField-DA_S6lQV.js +3 -0
  67. package/dist/UrlField-D0ugmD3f.cjs +1 -0
  68. package/dist/UrlField-DCnXHR1A.js +3 -0
  69. package/dist/UserField-DGlorzyG.cjs +1 -0
  70. package/dist/UserField-V0GmvyGV.js +3 -0
  71. package/dist/VectorField-BSD85ntU.cjs +1 -0
  72. package/dist/VectorField-CVi5yrrc.js +3 -0
  73. package/dist/fields/src/index.d.ts +1 -0
  74. package/dist/fields/src/record-picker-dialog.stories.d.ts +25 -0
  75. package/dist/fields/src/record-picker.test.d.ts +1 -0
  76. package/dist/fields/src/widgets/RecordPickerDialog.d.ts +166 -0
  77. package/dist/fields/src/widgets/_cell-renderer-bridge.d.ts +3 -0
  78. package/dist/index.cjs +4 -9
  79. package/dist/index.js +2229 -2850
  80. package/dist/jsx-runtime-DHCF5ANT.cjs +6 -0
  81. package/dist/jsx-runtime-MxADXuRH.js +189 -0
  82. package/package.json +7 -6
  83. package/src/index.tsx +6 -0
  84. package/src/record-picker-dialog.stories.tsx +160 -0
  85. package/src/record-picker.test.tsx +1696 -0
  86. package/src/widgets/LookupField.tsx +254 -150
  87. package/src/widgets/RecordPickerDialog.tsx +1182 -0
  88. package/src/widgets/_cell-renderer-bridge.ts +19 -0
@@ -1,17 +1,19 @@
1
1
  import React, { useState, useEffect, useCallback, useRef, useContext, useMemo } from 'react';
2
2
  import {
3
3
  Button,
4
- Dialog,
5
- DialogContent,
6
- DialogHeader,
7
- DialogTitle,
8
- DialogTrigger,
9
4
  Input,
10
- Badge
5
+ Badge,
6
+ Popover,
7
+ PopoverTrigger,
8
+ PopoverContent,
11
9
  } from '@object-ui/components';
12
- import { Search, X, Loader2, AlertCircle, Plus } from 'lucide-react';
10
+ import { Search, X, Loader2, AlertCircle, Plus, TableProperties } from 'lucide-react';
13
11
  import { FieldWidgetProps } from './types';
14
- import type { DataSource, QueryParams } from '@object-ui/types';
12
+ import type { DataSource, QueryParams, LookupColumnDef } from '@object-ui/types';
13
+ import { RecordPickerDialog } from './RecordPickerDialog';
14
+ import type { RecordPickerFilterColumn } from './RecordPickerDialog';
15
+ import { getCellRendererResolver } from './_cell-renderer-bridge';
16
+ import { SchemaRendererContext as ImportedSchemaRendererContext } from '@object-ui/react';
15
17
 
16
18
  export interface LookupOption {
17
19
  value: string | number;
@@ -20,25 +22,14 @@ export interface LookupOption {
20
22
  [key: string]: any;
21
23
  }
22
24
 
23
- /** Default page size for lookup data fetching */
25
+ /** Page size for the quick-select popover typeahead */
24
26
  const LOOKUP_PAGE_SIZE = 50;
25
27
 
26
28
  /**
27
- * Resolve SchemaRendererContext from @object-ui/react at runtime.
28
- * Uses the same dynamic-require fallback that plugin-view uses to avoid
29
- * a hard dependency on @object-ui/react (which would create a cycle).
29
+ * SchemaRendererContext is created by @object-ui/react.
30
+ * Using a static import to be compatible with Next.js Turbopack SSR.
30
31
  */
31
- const FallbackContext = React.createContext<any>(null);
32
- let SchemaRendererContext: React.Context<any> = FallbackContext;
33
- try {
34
- // eslint-disable-next-line @typescript-eslint/no-require-imports
35
- const mod = require('@object-ui/react');
36
- if (mod.SchemaRendererContext) {
37
- SchemaRendererContext = mod.SchemaRendererContext;
38
- }
39
- } catch {
40
- // @object-ui/react not available — dataSource must be passed via props
41
- }
32
+ const SchemaRendererContext: React.Context<any> = ImportedSchemaRendererContext;
42
33
 
43
34
  /**
44
35
  * Map a raw record to a LookupOption using a display field and an id field.
@@ -55,6 +46,27 @@ function recordToOption(
55
46
  return { value: val, label: String(label), description, ...record };
56
47
  }
57
48
 
49
+ /**
50
+ * Map a LookupColumnDef.type to a filter input type for the filter bar.
51
+ * Returns undefined if the field type is not filterable.
52
+ */
53
+ function mapFieldTypeToFilterType(
54
+ fieldType: string,
55
+ ): RecordPickerFilterColumn['type'] | undefined {
56
+ const mapping: Record<string, RecordPickerFilterColumn['type']> = {
57
+ text: 'text',
58
+ number: 'number',
59
+ currency: 'number',
60
+ percent: 'number',
61
+ select: 'select',
62
+ status: 'select',
63
+ date: 'date',
64
+ datetime: 'date',
65
+ boolean: 'boolean',
66
+ };
67
+ return mapping[fieldType];
68
+ }
69
+
58
70
  /**
59
71
  * Lookup field for selecting related records.
60
72
  * Supports single and multi-select with search.
@@ -75,6 +87,11 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
75
87
  const [totalCount, setTotalCount] = useState(0);
76
88
  const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
77
89
 
90
+ // Records selected via RecordPickerDialog (Level 2).
91
+ // Stored as LookupOption so that findOption can resolve display labels
92
+ // even when the record wasn't part of the Level 1 popover fetch.
93
+ const [pickerResolvedRecords, setPickerResolvedRecords] = useState<LookupOption[]>([]);
94
+
78
95
  // Arrow-key active index (-1 = none)
79
96
  const [activeIndex, setActiveIndex] = useState(-1);
80
97
  const listRef = useRef<HTMLDivElement>(null);
@@ -99,6 +116,30 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
99
116
  // ObjectStack convention uses `reference`; types define `reference_to` — support both
100
117
  const referenceTo: string | undefined = fieldMeta?.reference_to || fieldMeta?.reference;
101
118
 
119
+ // Enterprise Record Picker configuration
120
+ const lookupColumns: Array<string | LookupColumnDef> | undefined = fieldMeta?.lookup_columns;
121
+ const lookupPageSize: number | undefined = fieldMeta?.lookup_page_size;
122
+ const lookupFilters: import('@object-ui/types').LookupFilterDef[] | undefined = fieldMeta?.lookup_filters;
123
+
124
+ // Derive filter columns from lookup_columns that have type info
125
+ const filterColumns = useMemo<RecordPickerFilterColumn[] | undefined>(() => {
126
+ if (!lookupColumns) return undefined;
127
+ const cols: RecordPickerFilterColumn[] = [];
128
+ for (const c of lookupColumns) {
129
+ if (typeof c === 'object' && c.type) {
130
+ const filterType = mapFieldTypeToFilterType(c.type);
131
+ if (filterType) {
132
+ cols.push({
133
+ field: c.field,
134
+ label: c.label,
135
+ type: filterType,
136
+ });
137
+ }
138
+ }
139
+ }
140
+ return cols.length > 0 ? cols : undefined;
141
+ }, [lookupColumns]);
142
+
102
143
  // Resolve DataSource: explicit prop > field-level > wrapper field > SchemaRendererContext > none
103
144
  const ctx = useContext(SchemaRendererContext);
104
145
  const contextDataSource = ctx?.dataSource ?? null;
@@ -111,6 +152,9 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
111
152
  const onCreateNew: ((searchQuery: string) => void) | undefined =
112
153
  (props as any).onCreateNew ?? lookupField?.onCreateNew;
113
154
 
155
+ // State for the full Record Picker dialog (Level 2)
156
+ const [isPickerOpen, setIsPickerOpen] = useState(false);
157
+
114
158
  // Determine which options to display
115
159
  const allOptions = hasDataSource ? fetchedOptions : staticOptions;
116
160
 
@@ -206,15 +250,16 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
206
250
  };
207
251
  }, []);
208
252
 
209
- // Get selected option(s) — check both static and fetched options
253
+ // Get selected option(s) — check static, fetched, and picker-resolved options
210
254
  const findOption = useCallback(
211
255
  (v: any): LookupOption | undefined => {
212
256
  return (
213
257
  staticOptions.find(opt => opt.value === v) ??
214
- fetchedOptions.find(opt => opt.value === v)
258
+ fetchedOptions.find(opt => opt.value === v) ??
259
+ pickerResolvedRecords.find(opt => opt.value === v)
215
260
  );
216
261
  },
217
- [staticOptions, fetchedOptions],
262
+ [staticOptions, fetchedOptions, pickerResolvedRecords],
218
263
  );
219
264
 
220
265
  const selectedOptions = multiple
@@ -249,6 +294,16 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
249
294
  }
250
295
  };
251
296
 
297
+ // Callback from RecordPickerDialog — caches selected records so that
298
+ // findOption can resolve display labels after the dialog closes.
299
+ const handlePickerSelectRecords = useCallback(
300
+ (records: any[]) => {
301
+ const mapped = records.map(r => recordToOption(r, displayField, idField, descriptionField));
302
+ setPickerResolvedRecords(mapped);
303
+ },
304
+ [displayField, idField, descriptionField],
305
+ );
306
+
252
307
  // Keyboard handler for the search input — arrow keys + Enter
253
308
  const handleSearchKeyDown = useCallback(
254
309
  (e: React.KeyboardEvent) => {
@@ -327,12 +382,13 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
327
382
  </div>
328
383
  )}
329
384
 
330
- {/* Lookup dialog trigger */}
331
- <Dialog open={isOpen} onOpenChange={setIsOpen}>
332
- <DialogTrigger asChild>
385
+ {/* Level 1: Quick-select Popover (inline typeahead) */}
386
+ <div className="flex items-center gap-1.5">
387
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
388
+ <PopoverTrigger asChild>
333
389
  <Button
334
390
  variant="outline"
335
- className="w-full justify-start text-left font-normal"
391
+ className="min-w-0 flex-1 justify-start text-left font-normal"
336
392
  type="button"
337
393
  >
338
394
  <Search className="mr-2 size-4" />
@@ -341,16 +397,10 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
341
397
  : multiple ? `${selectedOptions.length} selected` : 'Change selection'
342
398
  }
343
399
  </Button>
344
- </DialogTrigger>
345
- <DialogContent className="max-w-md">
346
- <DialogHeader>
347
- <DialogTitle>
348
- {lookupField?.label || 'Select'} {multiple && '(multiple)'}
349
- </DialogTitle>
350
- </DialogHeader>
351
-
400
+ </PopoverTrigger>
401
+ <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
352
402
  {/* Search input */}
353
- <div className="space-y-4">
403
+ <div className="p-2">
354
404
  <div className="relative">
355
405
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
356
406
  <Input
@@ -358,7 +408,7 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
358
408
  value={searchQuery}
359
409
  onChange={(e) => handleSearchChange(e.target.value)}
360
410
  onKeyDown={handleSearchKeyDown}
361
- className="w-full pl-9"
411
+ className="w-full pl-9 h-8 text-sm"
362
412
  />
363
413
  {loading && (
364
414
  <Loader2
@@ -367,121 +417,175 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
367
417
  />
368
418
  )}
369
419
  </div>
420
+ </div>
370
421
 
371
- {/* Error state */}
372
- {error && (
373
- <div className="flex flex-col items-center gap-2 py-4" role="alert">
374
- <AlertCircle className="size-5 text-destructive" />
375
- <p className="text-sm text-destructive">{error}</p>
376
- <Button
377
- variant="outline"
378
- size="sm"
379
- onClick={() => fetchLookupData(searchQuery || undefined)}
380
- type="button"
381
- >
382
- Retry
383
- </Button>
384
- </div>
385
- )}
386
-
387
- {/* Loading state (initial load only, not search refinement) */}
388
- {loading && filteredOptions.length === 0 && !error && (
389
- <div className="flex flex-col items-center gap-2 py-6" role="status" aria-live="polite">
390
- <Loader2 className="size-6 animate-spin text-muted-foreground" />
391
- <p className="text-sm text-muted-foreground">Loading…</p>
392
- </div>
393
- )}
394
-
395
- {/* Options list */}
396
- {!error && !(loading && filteredOptions.length === 0) && (
397
- <div ref={listRef} className="max-h-64 overflow-y-auto space-y-1" role="listbox">
398
- {filteredOptions.length === 0 ? (
399
- <div className="py-4 text-center">
400
- <p className="text-sm text-muted-foreground">
401
- No options found
402
- </p>
403
- {/* Quick-create entry */}
404
- {onCreateNew && (
405
- <Button
406
- variant="ghost"
407
- size="sm"
408
- className="mt-2 gap-1"
409
- type="button"
410
- onClick={() => {
411
- onCreateNew(searchQuery);
412
- setIsOpen(false);
413
- }}
414
- >
415
- <Plus className="size-4" />
416
- Create new
417
- </Button>
418
- )}
419
- </div>
420
- ) : (
421
- <>
422
- {filteredOptions.map((option, idx) => {
423
- const isSelected = multiple
424
- ? (Array.isArray(value) ? value : []).includes(option.value)
425
- : value === option.value;
426
- const isActive = idx === activeIndex;
427
-
428
- return (
429
- <button
430
- key={option.value}
431
- data-lookup-index={idx}
432
- role="option"
433
- aria-selected={isSelected}
434
- onClick={() => handleSelect(option)}
435
- className={`w-full text-left px-3 py-2 rounded-md text-sm hover:bg-accent flex items-center justify-between ${
436
- isActive
437
- ? 'bg-accent text-accent-foreground'
438
- : isSelected
439
- ? 'bg-accent/50 text-accent-foreground'
440
- : ''
441
- }`}
442
- type="button"
443
- >
444
- <div className="min-w-0 flex-1">
445
- <span className="block truncate">{option.label}</span>
446
- {option.description && (
447
- <span className="block truncate text-xs text-muted-foreground">
448
- {option.description}
449
- </span>
450
- )}
451
- </div>
452
- {isSelected && (
453
- <Badge variant="default" className="ml-2 shrink-0">Selected</Badge>
454
- )}
455
- </button>
456
- );
457
- })}
458
- {/* Show total count when fetched from DataSource */}
459
- {hasDataSource && totalCount > filteredOptions.length && (
460
- <p className="text-xs text-muted-foreground text-center py-2">
461
- Showing {filteredOptions.length} of {totalCount} results. Refine your search to find more.
462
- </p>
463
- )}
464
- {/* Quick-create entry (below results) */}
465
- {onCreateNew && (
422
+ {/* Error state */}
423
+ {error && (
424
+ <div className="flex flex-col items-center gap-2 py-4 px-2" role="alert">
425
+ <AlertCircle className="size-5 text-destructive" />
426
+ <p className="text-sm text-destructive">{error}</p>
427
+ <Button
428
+ variant="outline"
429
+ size="sm"
430
+ onClick={() => fetchLookupData(searchQuery || undefined)}
431
+ type="button"
432
+ >
433
+ Retry
434
+ </Button>
435
+ </div>
436
+ )}
437
+
438
+ {/* Loading state (initial load only, not search refinement) */}
439
+ {loading && filteredOptions.length === 0 && !error && (
440
+ <div className="flex flex-col items-center gap-2 py-6" role="status" aria-live="polite">
441
+ <Loader2 className="size-6 animate-spin text-muted-foreground" />
442
+ <p className="text-sm text-muted-foreground">Loading…</p>
443
+ </div>
444
+ )}
445
+
446
+ {/* Options list */}
447
+ {!error && !(loading && filteredOptions.length === 0) && (
448
+ <div ref={listRef} className="max-h-64 overflow-y-auto px-1 pb-1" role="listbox">
449
+ {filteredOptions.length === 0 ? (
450
+ <div className="py-4 text-center">
451
+ <p className="text-sm text-muted-foreground">
452
+ No options found
453
+ </p>
454
+ {/* Quick-create entry */}
455
+ {onCreateNew && (
456
+ <Button
457
+ variant="ghost"
458
+ size="sm"
459
+ className="mt-2 gap-1"
460
+ type="button"
461
+ onClick={() => {
462
+ onCreateNew(searchQuery);
463
+ setIsOpen(false);
464
+ }}
465
+ >
466
+ <Plus className="size-4" />
467
+ Create new
468
+ </Button>
469
+ )}
470
+ </div>
471
+ ) : (
472
+ <>
473
+ {filteredOptions.map((option, idx) => {
474
+ const isSelected = multiple
475
+ ? (Array.isArray(value) ? value : []).includes(option.value)
476
+ : value === option.value;
477
+ const isActive = idx === activeIndex;
478
+
479
+ return (
466
480
  <button
481
+ key={option.value}
482
+ data-lookup-index={idx}
483
+ role="option"
484
+ aria-selected={isSelected}
485
+ onClick={() => handleSelect(option)}
486
+ className={`w-full text-left px-3 py-2 rounded-md text-sm hover:bg-accent flex items-center justify-between ${
487
+ isActive
488
+ ? 'bg-accent text-accent-foreground'
489
+ : isSelected
490
+ ? 'bg-accent/50 text-accent-foreground'
491
+ : ''
492
+ }`}
467
493
  type="button"
468
- className="w-full text-left px-3 py-2 rounded-md text-sm hover:bg-accent flex items-center gap-1.5 text-muted-foreground"
469
- onClick={() => {
470
- onCreateNew(searchQuery);
471
- setIsOpen(false);
472
- }}
473
494
  >
474
- <Plus className="size-3.5" />
475
- Create new{searchQuery ? ` "${searchQuery}"` : ''}
495
+ <div className="min-w-0 flex-1">
496
+ <span className="block truncate">{option.label}</span>
497
+ {option.description && (
498
+ <span className="block truncate text-xs text-muted-foreground">
499
+ {option.description}
500
+ </span>
501
+ )}
502
+ </div>
503
+ {isSelected && (
504
+ <Badge variant="default" className="ml-2 shrink-0">Selected</Badge>
505
+ )}
476
506
  </button>
477
- )}
478
- </>
479
- )}
480
- </div>
481
- )}
482
- </div>
483
- </DialogContent>
484
- </Dialog>
507
+ );
508
+ })}
509
+ {/* Show total count when fetched from DataSource */}
510
+ {hasDataSource && totalCount > filteredOptions.length && (
511
+ <p className="text-xs text-muted-foreground text-center py-2">
512
+ Showing {filteredOptions.length} of {totalCount} results.
513
+ </p>
514
+ )}
515
+ {/* "Show All Results" button — opens the full Record Picker (Level 2) */}
516
+ {hasDataSource && totalCount > filteredOptions.length && (
517
+ <button
518
+ type="button"
519
+ className="w-full text-center px-3 py-2 rounded-md text-sm font-medium text-primary hover:bg-accent flex items-center justify-center gap-1.5"
520
+ onClick={() => {
521
+ setIsOpen(false);
522
+ setIsPickerOpen(true);
523
+ }}
524
+ data-testid="show-all-results"
525
+ >
526
+ <TableProperties className="size-3.5" />
527
+ Show All Results ({totalCount})
528
+ </button>
529
+ )}
530
+ {/* Quick-create entry (below results) */}
531
+ {onCreateNew && (
532
+ <button
533
+ type="button"
534
+ className="w-full text-left px-3 py-2 rounded-md text-sm hover:bg-accent flex items-center gap-1.5 text-muted-foreground"
535
+ onClick={() => {
536
+ onCreateNew(searchQuery);
537
+ setIsOpen(false);
538
+ }}
539
+ >
540
+ <Plus className="size-3.5" />
541
+ Create new{searchQuery ? ` "${searchQuery}"` : ''}
542
+ </button>
543
+ )}
544
+ </>
545
+ )}
546
+ </div>
547
+ )}
548
+ </PopoverContent>
549
+ </Popover>
550
+
551
+ {/* "Browse All" button — always visible when DataSource is available */}
552
+ {hasDataSource && (
553
+ <Button
554
+ variant="outline"
555
+ size="icon"
556
+ className="shrink-0"
557
+ type="button"
558
+ onClick={() => setIsPickerOpen(true)}
559
+ aria-label="Browse all records"
560
+ title="Browse all records"
561
+ data-testid="browse-all-records"
562
+ >
563
+ <TableProperties className="size-4" />
564
+ </Button>
565
+ )}
566
+ </div>
567
+
568
+ {/* Level 2: Full Record Picker Dialog */}
569
+ {hasDataSource && dataSource && referenceTo && (
570
+ <RecordPickerDialog
571
+ open={isPickerOpen}
572
+ onOpenChange={setIsPickerOpen}
573
+ title={lookupField?.label || 'Select'}
574
+ multiple={multiple}
575
+ dataSource={dataSource}
576
+ objectName={referenceTo}
577
+ columns={lookupColumns}
578
+ displayField={displayField}
579
+ idField={idField}
580
+ pageSize={lookupPageSize}
581
+ value={value}
582
+ onSelect={onChange}
583
+ onSelectRecords={handlePickerSelectRecords}
584
+ lookupFilters={lookupFilters}
585
+ cellRenderer={getCellRendererResolver()}
586
+ filterColumns={filterColumns}
587
+ />
588
+ )}
485
589
  </div>
486
590
  );
487
591
  }