Skip to content

feat: deterministic record lookups, configurable search minimum, and an option-count search threshold [3.x] - #205

Merged
ManukMinasyan merged 6 commits into
3.xfrom
feat/select-picker-behavior
Aug 20, 2026
Merged

feat: deterministic record lookups, configurable search minimum, and an option-count search threshold [3.x]#205
ManukMinasyan merged 6 commits into
3.xfrom
feat/select-picker-behavior

Conversation

@ManukMinasyan

@ManukMinasyan ManukMinasyan commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Three changes to the picker paths, all reachable from a new selects config block so 3.7 behavior can be restored exactly.

1. Deterministic record lookups

RecordSelectInputComponent::getInitialOptions() and searchRecords() ran limit(50) with no orderBy, so the initial page was whatever the database happened to return and could differ between two renders of the same form.

Both now order by the model key descending, which is deterministic and index-backed. A configured column is ordered by first with the key appended as a tiebreaker, since rows sharing a timestamp are the normal case in bulk-imported data.

Column existence is resolved without a schema query, since a runtime Schema::hasColumn() call would be a per-request round trip. A configured column is trusted, with one documented exception: 'updated_at' on a model that returns false from usesTimestamps() falls back to the key.

Driver-neutral: plain orderBy, no raw SQL.

2. Minimum search length, configurable end to end

getSearchResultsForJs() returned [] below 2 characters. It now returns the unfiltered first page, and the minimum comes from config rather than a literal.

On scope: the packaged Blade already filters the loaded first page client-side below the minimum, so this is not a visible "no results" fix in the shipped UI. The view had to change because the minimum was hardcoded as 2 in four places in the Alpine component. With a server-side-only knob, setting min_search_length to 3 would make the client request a filtered search at 2 characters and the server answer it with an unfiltered page of 50 records. The view now reads getMinSearchLength(), so client and server always agree.

3. Search box only above an option-count threshold (behavior change)

SelectComponent and MultiSelectComponent called ->searchable() unconditionally, so a three-option Status field rendered a search box above three items. They now gate on option count, default 10.

This is a taste change, not a bug fix. It carries no performance benefit: getSearchResultsUsing is only assigned inside relationship(), so hasDynamicSearchResults() is false for static options and filtering already happens client-side at zero server cost. The only effect is that the search box disappears on small option sets.

Opt-out: set custom-fields.selects.searchable_threshold to 0 to always render the search box, which is exactly the pre-3.8 behavior.

Ordering default, decided by measurement

The first cut of this PR defaulted record_lookup.order_column to 'updated_at'. Measured on a seeded 50,000-row tenant with EXPLAIN (ANALYZE, BUFFERS):

Variant Plan Time Shared buffers
updated_at desc, id desc Seq Scan 50,004 rows, top-N heapsort, Limit 7.59 ms 822
No ORDER BY (3.7 behaviour) Seq Scan stops at 50 rows 0.043 ms 4
Model key desc Index Scan Backward on the primary key 0.036 ms 7

Ordering by the model key is deterministic and costs the same as the unordered query it replaces, because the key is already the tiebreaker. Ordering by an unindexed updated_at sorts the whole tenant on every render.

So the default is now order_column => null, meaning the model key. Set it to 'updated_at' if you want most-recently-touched-first, and index that column when the lookup table is large.

Config

'selects' => [
    'searchable_threshold' => 10,   // 0 always shows the search box (pre-3.8 behavior)

    'record_lookup' => [
        'order_column' => null,          // null means the model key, index-backed
        'order_direction' => 'desc',
        'limit' => 50,
        'min_search_length' => 2,
    ],
],

Consumers who published config/custom-fields.php before 3.8 do not have this block. Every read passes the same value as its default, so nothing breaks and the new defaults apply.

Tests

15 new tests:

  • Ordering: most recent first, tie-break stability across repeated calls, the timestampless fallback, a configured column and direction, and the configured limit.
  • Search: the below-minimum response shape, normal filtering at the minimum, the empty-table case, a raised minimum, and a rendered-page assertion that the view receives the configured value.
  • Threshold: at the threshold, above it, the 0 opt-out, and the multi-select path.
  • Config: the five documented defaults.

The tiebreaker was verified as load-bearing by removing it and watching the stability test fail.

Verification

pint, phpstan, rector --dry-run and the full Pest suite pass (846 passed, 3 todos) against the dependency set CI resolves (Laravel 13, Filament 5.7.6).

The raised-minimum client behavior is covered by a render assertion rather than a browser click: this repo has no browser suite, and the consuming app installs from Packagist.

Copilot AI lite review requested due to automatic review settings August 20, 2026 18:48
@ManukMinasyan ManukMinasyan added the enhancement New feature or request label Aug 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@ManukMinasyan
ManukMinasyan force-pushed the feat/select-picker-behavior branch from b496c92 to fc822b5 Compare August 20, 2026 18:59
@ManukMinasyan
ManukMinasyan merged commit ad3a148 into 3.x Aug 20, 2026
5 checks passed
@ManukMinasyan
ManukMinasyan deleted the feat/select-picker-behavior branch August 20, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

2 participants