Skip to content

fix(app-shell): one shared object picker for Studio's object-name inputs, resolving only the chosen name (objectui#11783) - #11840

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11783-object-picker
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11783-object-picker

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11783

Clause-②: no

What this changes

Studio's object-name inputs now share one module-private picker, inspectors/ObjectPicker.tsx.

  • Who renders it. In ObjectFieldInspector: the Lookup's Related object and the summary's Child object. In FlowReferenceField: every flow reference of kind object, through one branch in ReferenceCombobox. That covers the trigger's Object, the time-relative Sweep object, the map node's Item object, the Object of the four record nodes and the legacy action node, and the screen's Object form. The trigger field reaches the picker through FlowNodeConfigField and FlowReferenceField. FlowNodeInspector.tsx is untouched.

  • When a name leaves it. Typing stays in the input. onCommit runs once, when the author chooses an option, presses Enter, or leaves the input after typing. Escape abandons the typing. Enter and blur commit exactly what was typed, matched or not, so a flow value that is an expression is accepted as before.

  • What it offers. One read per picker mount: GET /api/v1/meta/object?preview=draft. The groups come in this order:

    • this package's objects, where the package is Studio's :packageId route parameter, else the editor URL's ?package=;
    • then the other objects;
    • then every object the server marks isSystem, in a collapsed System group. A search reaches that group without opening it, so sys_user stays choosable for a Lookup.

    Each option shows its label and its name. An entry served with _draft carries the existing engine.inspector.draftSuffix marker. "System" is read from the served isSystem only: no name prefix and no list.

  • crm_lead. The placeholder is removed from the trigger Object field, with no replacement and no locale key.

  • i18n. Six new engine.inspector.objectPicker.* rows, EN and ZH, in metadata-admin/i18n.ts. Loading and load failure reuse engine.form.loadingOptions and engine.form.optionsLoadFailedTitle.

Nothing is added to the package's entry: no export, published prop, @object-ui/types member or packages/i18n key. One module-internal optional prop, ariaLabel on ReferenceCombobox, is added. It carries the flow field's label to the picker, so the input has an accessible name (the old datalist input had none). ReferenceCombobox is not exported from the package.

Why the preview list, and not the list the inputs read before

This departs from the suggested route on measurement:

  • What the draft list lacks. GET /meta/_drafts?type=object answers header rows with no label. Labelling a draft-only object from it would take one more read per draft.
  • What the preview list carries. ?preview=draft serves the draft-only object in the same single answer, with its own label, its _packageId and _draft: true.
  • Callers without draft access. A caller the server does not admit to pending drafts gets the published list. That is the server's documented behaviour (mayReadPendingDrafts in objectstack's runtime meta domain); this run did not measure it.
  • One consequence, also measured. A published object with a pending draft is served _draft: true with its draft label, so it also carries the draft marker.

Measurements

Setup. Real Chromium at 1440×900, with this branch's console on a dev-only Vite harness that is not committed. The backend is examples/app-showcase from objectstack bafb58bb, run with objectstack dev --seed-admin --fresh. The data is a writable package com.example.repairs holding a draft-only object repairs_repair_ticket and a draft flow. The author types repairs_repair_ticket one key at a time into each input. Both runs start from the same drafts.

before, 16cda0b after
Lookup Related object: per-prefix GET /meta/object/PREFIX while typing 20 0
Lookup: object reads on commit (one per keystroke) 1, for the committed name
Flow trigger Object: per-prefix GET /meta/object/PREFIX while typing 41 0
Flow: object reads on commit (one or two per keystroke) 3, all for the committed name
What the input offers datalist of 109 / 108 entries, 83 named sys_*, the draft as a bare name 2 package + 27 other, then a collapsed System group; the draft as Repair Ticket (draft)
Trigger placeholder crm_lead none

All 3 reads on the flow commit are for the committed name. The inspector holds more than one reader of the trigger object: useFlowScope in FlowNodeInspector and the entry-condition ConditionBuilder are two of them, and the third was not identified. All of them are outside this card's files.

H2, the served list. GET /api/v1/meta/object returned 107 entries. All 107 carry isSystem (80 true) and a label, and every entry carries _packageId. A runtime-authored object carries no isSystem key, published or draft. Its absence is the spec's declared default, false. Three platform objects are served isSystem: false: sys_inbox_message, sys_file and sys_upload_session. They are offered up front, because the mark decides, not the prefix.

H3, the object-name inputs in the Studio inspectors.

  • Moved onto the picker: the Lookup and summary inputs above, and every flow reference of kind object. Each was a free-text datalist that committed per keystroke.

  • Left as they are, because they are not the same kind:

    • the hook's Objects, a multi-select checkbox list;
    • the action's Object and the page block's object, both strict selects (each falls back to a text field only when the list is empty);
    • the view variant's, dataset's and app-nav's object pickers, all InspectorComboField popovers that commit on choice.

    None of these fetches per keystroke. All of them still list isSystem objects without grouping (see Acceptance notes).

H4, the draft object. repairs_repair_ticket is offered in This package, labelled Repair Ticket from its draft and marked as a draft. It is choosable from both inputs.

Tests

New pins:

  • ObjectPicker.test.tsx checks what the picker offers and in which order. It checks that the served isSystem decides membership in System: sys_file is offered up front and audit_trail is collapsed. It checks that opening System or searching reaches those objects, and that the draft is labelled and marked. It checks that typing commits nothing, that Enter and blur commit what was typed (an expression included), that Escape abandons, and that the catalog is read once.
  • ObjectFieldInspector.objectPicker-11783.test.tsx runs the real inspector with the real useObjectFields. Typing the name makes 0 object reads, and the commit makes 1, for that name. sys_user is choosable.
  • FlowReferenceField.objectPicker-11783.test.tsx runs the real FlowNodeInspector, with the real useFlowScope and useObjectFields. Typing makes 0 object reads, and the commit reads only the committed name. An expression value is accepted as before (a control). The field has no crm_lead placeholder.

Updated: ObjectFieldInspector.draft-locale.test.tsx reads the localized draft marker on the picker. ObjectFieldInspector.test.tsx's summary case commits by leaving the input.

Gates, run locally on HEAD 8b94ce9:

  • every test file importing a touched module, 102 files, run from the root with pnpm exec vitest run: exit 0, Test Files 102 passed (102), Tests 1206 passed | 1 skipped (1207)
  • pnpm --filter @object-ui/app-shell type-check, which runs tsc --noEmit && tsc -p tsconfig.test.json (--listFilesOnly confirms the new test files are in that project): exit 0
  • check:control-bytes · check:new-line-citations · check:changeset-claims · check:pending-changeset-literals: exit 0
  • the app-shell locale checks check:i18n-designer-parity and check:i18n-dead-keys (designer-table leg), plus check:i18n-keys: exit 0
  • added for this diff (new vi.mock doubles, a new source file, a new react-router-dom import): check:vi-mock-specifiers · check:vi-mock-inherit · check:vi-mock-override-shape · check:unreferenced-sources · check:phantom-deps · check:test-path-roots · check-changeset-presence.mjs: exit 0
  • eslint --no-inline-config over the changed files: 0 errors. ObjectPicker.tsx has no warnings, and FlowReferenceField.tsx carries the same 10 warnings it had at 16cda0b.

The repo-wide pnpm lint and the full test farm are left to CI.

Reverse check. This ran on the committed fix, 3389363. The three source files were reverted to 16cda0b, the two inspector-level pin files were run, and the files were restored from HEAD. The restore was proven by blob hashes equal to HEAD and an empty git diff HEAD. Predicted and observed, 4 red and 2 green:

  • the Lookup typing pin received 21 reads (20 prefixes and 1 full name) where 0 were expected;
  • the flow typing pin received 42 reads (40 prefixes and 2 full names);
  • the sys_user pin and the placeholder pin went red;
  • the two controls, the saved Lookup reference and the accepted expression value, stayed green.

Acceptance notes

Noted here, not filed:

  • Other flow placeholders. contract (the four record nodes and the legacy action node), contracts (sweep object) and crm_account (screen object form) name objects the showcase does not serve. showcase_task (map item object) exists only in the showcase. The view inspector's engine.inspector.view.objectPlaceholder reads e.g. crm_lead. This is the same class as crm_lead, but outside this claim's lines.
  • Other object pickers. The hook, action, page-block, view-variant, dataset and app-nav pickers still list isSystem objects first-class and ungrouped. The action's and page block's empty-list text-field fallback commits per keystroke, which reaches useObjectFields.
  • Draft objects in the rail. Studio's Data rail lists a draft-only object by bare name, without its draft's label.
  • Platform objects unmarked. sys_inbox_message, sys_file and sys_upload_session are served isSystem: false. Whether they should carry the mark is the platform's decision.
  • Payload. The object list read is a full-body list, about 670 KB on the showcase. The inputs read a list of the same size before this change.

Generated by Claude Code

claude added 3 commits October 7, 2026 23:31
…uts, resolving only the chosen name (objectui#11783)

The Lookup's Related object, the summary's child object and every flow
reference of kind `object` now render one module-private ObjectPicker.

- Typing stays in the input; the name is committed on a choice, Enter or
  blur, so nothing that resolves it (useObjectFields, useFlowScope, the
  entry-condition builder) refetches per keystroke. Escape abandons.
- Enter and blur commit exactly what was typed, so a flow value that is
  an expression is accepted as before.
- One catalog read per mount, the draft-overlaid object list: this
  package's objects first, then the others, with label and name; served
  isSystem objects sit in a collapsed System group a search still reaches.
  A draft-only object is offered with its draft's label.
- The trigger's Object field drops its crm_lead placeholder.

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
… lines

In the inspector's narrow column the two shared one line and both were cut
short ("Field Tec…", "Repair …"). The label now takes the first line, with
the draft marker; the name sits under it.

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
The hook has no caller outside the picker, and exporting it beside the
component defeats fast refresh. The not-yet-answered catalog is one stable
empty list rather than a fresh array each render, which the memoised
groups depended on.

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3524.7 KB 3551.8 KB
Main entry chunk (gzip) 157.0 KB 350 KB
Entry file index-CS2kUaGu.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 579.68KB 139.46KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.41KB 65.46KB
fields (index.js) 266.88KB 67.46KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 38.37KB 10.31KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.82KB 39.17KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.86KB 65.27KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 239.75KB 65.92KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.07KB 22.93KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 00:19
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 00:19
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 57dbe34 Oct 8, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11783-object-picker branch October 8, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

studio: object-name inputs (Lookup Related object, flow trigger Object) fetch /meta/object/<prefix> on every keystroke and list internal sys_* tables

2 participants