Repository navigation
fix(plugin-list, fields): no quick-search or picker type-ahead on an object whose search resolves to no field (objectui#12966) - #12974
Merged
objectstack-fleet[bot] merged 2 commits intoOct 18, 2026
Conversation
…object whose search resolves to no field (objectui#12966) objectstack answers a non-blank search over such an object with 400 INVALID_FIELD. The list toolbar and the record pickers now decide with the spec's resolveSearchFieldResolution over the object definition (internal fields left out, the object's own searchableFields), and a refused search on a list shows the server's message. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
…read; declare a searchable field on the 12423 toolbar fixture (objectui#12966) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This was referenced Oct 18, 2026
objectstack-fleet
Bot
deleted the
claude/issue-12966-searchless-object
branch
October 18, 2026 16:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12966
Clause-②: no
What
objectstack now answers a non-blank
searchover an object whose resolved search set is empty with400 INVALID_FIELD(param: 'search'), where it used to answer every row (objectstack-ai/objectstack#24320,ec749fbb44). The console offered that search in three places. This PR stops offering it there, and shows the server's message where a box still shows and the search is refused.@object-ui/plugin-listListView).showSearchnow also requires that the object's search resolves to at least one field. A term restored at mount (initialSearchTerm) is not applied on such an object, the same two-readers rulesearchlessViewOnScreenalready follows.@object-ui/fields).RecordPickerDialogdraws no search box on a searchless target, and still lists and pages the records.LookupField's inline dropdown draws no type-ahead box, and still offers the records. Its options are buttons, so they stay reachable by Tab without the combobox input.ListView's load-error panel shows the server's sentence, which names the object and both remedies, when the failed request carried a term and the error'scodeisINVALID_FIELD. Before, it showed the generic "The server could not process this view's filter or query options" copy. The record pickers already renderederr.message, and so didObjectGrid's load-error panel (loadError.message), so neither changed.How "searchable" is decided
The decision uses the spec's own
resolveSearchFieldResolution(@objectstack/spec/data) over the inputs the engine gives it inObjectQL.expandSearchOnAst: the object's field map with everyinternal: truefield left out, and the OBJECT's declaredsearchableFields. No list of searchable types lives in objectui.displayFieldis not passed, because it only orders the auto-default set and never changes who is in it. The call sits in two small helpers,objectResolvesSearchinpackages/fields/src/widgets/searchOffer.ts(not re-exported from the barrel) and its twin inListView.tsx. Each is about twelve lines and both call the same spec resolver. I did not add a shared export, to stay inside the claimed file surface and keepClause-②: no.Measured assumptions
@objectstack/specis18.0.0-next.3(node_modules/@objectstack/spec/package.json). Itsdist/data/index.d.tsdeclaresresolveSearchFieldResolution(opts: Omit of SearchFieldResolutionOptions minus requestedFields)returning{ allowed, source }, the signature the engine and the ingress gate call.ListView: the object definition comes fromdataSource.getObjectSchemain the "Fetch object definition" effect. While it is unread, and when the data source serves none, the box is drawn, as before the card. On a searchless object the box is withdrawn when the read lands. The list is in its loading state for that read anyway, because its data fetch waits onobjectDefLoaded. I first gated the box on the read settling, so it would never draw and then withdraw. That made the box arrive asynchronously on every list, and it brokeapp-shell'slistViewGroupedSearch-11021test, which types into the box right after render. That file is outside the claimed surface, so I chose "keep today's behaviour until the schema is known". The cost is a possible brief box on a searchless object, only for the length of the definition read.RecordPickerDialog: theobjectSchemaprop (fromLookupFieldit isrefObjectSchema). When it is absent the box is kept. That coversRelatedList,PositionHoldersSection,AccessExplainPanelandAssignedUsersSection, which pass no schema.LookupFieldinline dropdown:refObjectSchema, read at mount. The dropdown opens on a click after that, so a flash is unlikely. Until the schema is known the box is kept.data-objectstackroutes every request carrying$searchthroughrawFindWithPopulate. On a non-OK answer that path throws anErrorwhosemessageis the envelope'serror.message, withcode(hereINVALID_FIELD),statusandhttpStatusset.classifyLoadErrorreads that asrejected. The message therefore reachesListView,ObjectGridanduseRecordQueryunchanged, anddata-objectstackis not touched. The envelope'sparamis not carried, soListViewkeys on "the request carried a term" pluscode === 'INVALID_FIELD'.Conflict with the order's pin (c), reported rather than chosen silently
The order says a VIEW-declared
searchableFieldsnaming a stored field should count, "exactly as the resolver says", and that such a view on an otherwise searchless object should draw the box. The engine does not do this. The view's list is sent as the$searchFieldsoverride. The ingress gateassertSearchFieldsAreSearchable(@objectstack/metadata-protocol) refuses any entry outside the object's resolved allowed set, and the linter'ssearchable-field-unsearchablerefuses the same view at build time. Objectstack#24320's pins also show that an explicitsearchFieldsdoes not rescue a searchless object. A box drawn there would therefore 400 on every term.So this PR pins the resolver's declared branch at the OBJECT level instead: an object that declares
searchableFields: ['book'], a lookup the auto-default skips, draws the box. A view-level list on a searchless object does not. The report carries this as an open question.Pins
packages/plugin-list/src/__tests__/ListView.searchlessObject-12966.test.tsx:searchableFieldsnaming a stored lookup draws it;internal: truetext field does not count;searchableFieldsdoes not rescue a searchless object;list-error-state(data-error-kind="rejected"), with the box still present to clear the term. CONTROL: a 400 with another code keeps the generic copy.packages/fields/src/widgets/RecordPickerDialog.searchlessTarget-12966.test.tsx:record-picker-search, lists records, and pages them (page 2 asks for$skip: 5), with no$searchsent;alertinstead of rows;LookupFielddropdown on a searchless target has nocomboboxand still offers options, with a CONTROL on a text target.packages/fields/src/widgets/searchOffer.test.tspins the helper: the junction, text and enum controls, the declared lookup against the declared formula,internal, the array field-map form, andundefinedfor an unknown schema.Fixture triage.
ListView.displayPopover-12423.test.tsxservedfields: {}, an object with no field at all, so it now resolves as searchless. That test asserts the full toolbar, including Search. I fixed it by adding a declaration: the fixture now declaresname: text. Its assertions are unchanged.Ablation
The fix was committed first (
3a00b9a653). Usingnode ../objectstack/scripts/ablation-replace.mjs,showSearch: ua?.search !== false && !searchlessViewOnScreen && !searchlessObject,becameshowSearch: ua?.search !== false && !searchlessViewOnScreen,, today's rule. The anchor count went from 1 to 0 and the replacement count from 0 to 1. The blob went fromb59af0273ea0to265c67830b8a.On the mutated source,
ListView.searchlessObject-12966.test.tsxgave 3 failed / 6 passed: (a), the internal-only case and the view-level case went red. The controls, the restored-term pair (it ridessearchTerm, notshowSearch) and (e) stayed green, which is the expected direction.Restore: the blob was
b59af0273ea0again, equal to HEAD, andgit diff HEADwas empty. An independentgit hash-objectafter the run matched HEAD's blob too.ListViewis imported from source, so no build was involved.Verification at
3a00b9a653pnpm --filter '@object-ui/plugin-list^...' build: exit 0, 13 projects.type-check(tsc --noEmit && tsc -p tsconfig.test.json) for@object-ui/fieldsand@object-ui/plugin-list: exit 0. Each package'stsconfig.test.jsonincludessrc/**/*.test.ts(x), so the new tests are type-checked.plugin-list,fields,plugin-gridandapp-shellthat namesshowSearch,search-icon-button,record-picker-search,searchableFields,$search,useRecordQueryorinitialSearchTerm. Plugin-list, fields and app-shell: 22 files and 388 tests passed after the timing change. Plugin-grid and app-shell: 15 files, all plugin-grid files passed. The 2 app-shell failures there came from the first gating, which was reverted, and the rerun above includes that file.packages/fields/directory: 270 files passed, 1 skipped; 4173 tests passed, 7 skipped.packages/plugin-list/directory: 152 files and 1540 tests passed.pnpm lintnarrowed to the 8 changed.ts/.tsxfiles (eslint --no-inline-config --format json): 0 errors across 8 files. The population is the packageeslint .config. The config enables no type-aware linting (noprojectServiceorparserOptions.projectineslint.config.js), so this diff cannot move any untouched file's verdict.check:phantom-deps,self-import,unreferenced-sources,doc-example-readers,handler-key-reads,side-effects-array,element-data-source-declaration,spec-symbols,action-forward-parity,designer-field-key-parity,icon-record-names,i18n-keys,i18n-drift,i18n-designer-parity,control-bytes,check-hand-rolled-comment-mask,check-changeset-fixed,check-changeset-presence,check:new-line-citations(0 new) andcheck:test-path-roots.Acceptance notes
PeoplePickeris not touched. Its target issys_user, whose name and email are textual, so its set is never empty.ObjectGridstandalone (a grid that owns its own toolbar search) still offers its box from the view, not from the resolver. The card scopedObjectGridto the refusal message only, and its error panel already rendersloadError.message. From reading the code, that panel returns before the data-table that draws the grid-owned box, so a standalone grid with a refused term shows the message but has no box left to clear the term with. This is read, not measured. Carrier: none.patchfor both packages. No export, prop or type member is added.Generated by Claude Code