Skip to content

feat(fields): offer the mobile shell's code scanner on text and lookup fields - #29493

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-6pv9qk
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-6pv9qk

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #29378

Clause-②: yes
Metadata-protocol: none

维护者速读

  • 改了什么:在 ObjectStack 移动壳内(桥接 has('scan') 为真时),可编辑的单行文本字段与查找(lookup)字段旁出现一个「扫码」图标按钮;文本字段把解码结果经 onChange 写入,查找字段用解码结果打开搜索。普通浏览器、旧版壳、只读或禁用字段不显示按钮。
  • 为什么改:移动壳已上线 scan()(feat(bridge): scan() — native barcode / QR scanner for Console fields mobile#170),Console 端没有调用方,就是「声明了却没人用」的能力;现场用户用手机录入资产标签、条码、单据号时需要它。
  • 风险与回滚:新增的公开面全部是加法:@object-ui/mobile 的 NativeHostMethod 加 'scan'、ObjectStackNative 加可选 scan?(opts)、新导出 NativeScanOptions 与 nativeScan(opts?);@object-ui/fields 的 PeoplePicker 加可选 prop initialSearch;@object-ui/i18n 十个语言包各加 fields.scan.button、fields.scan.failed 两个键;@object-ui/fields 新增依赖 @object-ui/mobile(mobile 只依赖 types,无环)。浏览器里按钮不渲染、DOM 不变。回滚即 revert 本 PR。
  • 先看:ui/packages/fields/src/widgets/ScanFieldButton.tsx(显示门 useFieldScanOffered 与失败 toast);ui/packages/fields/src/widgets/LookupField.tsx(handleScanned 与 lookupScan 的门控);ui/packages/mobile/src/nativeHost.ts(nativeScan)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/fields/src/widgets/ScanFieldButton.shellScan.test.tsx 应 13/13 通过;截图见下方 Verification(390 px,桩桥接)。
  • 需要你做的:无,席位落地。

What changes

  • @object-ui/mobile types the bridge's scan method, mirroring mobile docs/HYBRID-SHELL.md "Scanning codes": 'scan' joins NativeHostMethod; ObjectStackNative gains an OPTIONAL scan?(opts) (a shell older than the scanner does not carry it, and a required member would break typed stubs); NativeScanOptions (formats?: string[]) is exported; nativeScan(opts?) goes through callNative and resolves unavailable, ok with the decoded text or null (scanner closed), or failed with the shell's code and message. The method is called on the bridge, so the bridge stays its this.
  • @object-ui/fields gains a dependency on @object-ui/mobile (mobile depends only on @object-ui/types; plugin-grid, list, calendar and timeline already take this edge). An internal ScanFieldButton plus useFieldScanOffered() (not exported from the package entry) reads useNativeHost(), so a bridge the shell injects late re-renders the field with the button.
    • TextField: the single-line input only (not password, not the multi-row textarea, not disabled or read-only). The decoded text REPLACES the value through onChange, the same path a keystroke takes, so the host's validation judges it; it is not cut to maxLength, which would lose scanned data silently.
    • LookupField: the classic popover and the static-option list open with the code in their search box; the search-first variant (picker: 'search') opens PeoplePicker seeded through the new optional initialSearch prop, cleared when it closes. Gated like the trigger: no button when disabled, when dependsOn is unmet, in compact (inline cell) mode, or on a classic lookup whose target offers no search.
    • Outcomes: ok with text runs the handler; ok with null or unavailable does nothing; failed raises toast.error from @object-ui/components (the sonner instance the Console mounts) with the new key as title and the shell's message as description.
  • @object-ui/i18n: fields.scan.button ("Scan code") and fields.scan.failed ("Could not scan") in all ten locales, plus FIELD_DEFAULTS for a provider-less render.
  • No metadata key: the affordance follows the bridge capability, so Metadata-protocol: none.

Tests

  • New ScanFieldButton.shellScan.test.tsx (13 cases) with a stub window.ObjectStackNative: plain browser and has('scan') false render no button (text and lookup); read-only, disabled and password render none; the decoded value lands through onChange; null changes nothing and raises no toast; failed raises the toast with the shell message; a lookup's decoded value opens the dropdown with it in the search box and drives the $search query; null leaves a lookup closed; the search-first variant opens PeoplePicker seeded.
  • nativeHost.test.tsx extended: scan joins the unavailable / has-says-no / method-missing tables; ok with text (formats forwarded, bridge kept as this), ok with null, and failed with the shell message.
  • Ablation (scripts/ablation-replace.mjs, source-resolved through the vitest alias): replacing return host.has('scan') === true; with return (host.has('scan') || true) === true; in ScanFieldButton.tsx reds exactly the two older-shell pins (2 failed, 11 passed); restored, blob equal to HEAD and git diff HEAD empty.

Acceptance notes

  • Compact (inline grid cell) lookups get no scan button, matching the single-select clear control, which compact mode also omits.
  • The multi-select search-first lookup renders its row with display: contents, so the scan button sits on its own line under the trigger there.
  • check:dual-build-cjs-loads is NOT MEASURED locally (exit 3, needs a full pnpm build); it scans the root packages/ tree only, so no ui package is in its population. CI runs it.

Verification

Local runs on head bf41e3ae (suites, gates), then main merged at c6569d87 (main touched none of ui/packages/fields, mobile, i18n or the lockfile; the two scan test files re-ran green there: 2 files, 59 tests).

Suites and type-check
  • turbo run type-check for @object-ui/fields, @object-ui/mobile, @object-ui/i18n: 50 tasks successful (each package's tsc --noEmit && tsc -p tsconfig.test.json echoed).
  • pnpm --dir ui exec vitest run packages/mobile/ packages/i18n/: Test Files 104 passed, Tests 1567 passed, 13 skipped.
  • pnpm --dir ui exec vitest run packages/fields/: Test Files 294 passed, 1 skipped; Tests 4426 passed, 7 skipped.
  • pnpm --dir ui exec vitest run scripts/: Test Files 169 passed, 2 skipped; Tests 4921 passed.
Derived gates

node scripts/pm/dispatch-gates.mjs --commands derived 54 families; 53 exit 0, pnpm check:dual-build-cjs-loads exit 3 (prerequisite). --ran reconciliation: 54 accounted for, 53 run, 1 NOT-MEASURED, 0 UNRUN. node scripts/check-sdui-manifest.mjs exit 0. pnpm --dir ui run check:new-line-citations: 0 new citations.

Phone-width screenshots (390 px, stub bridge)

A temporary Console Vite entry rendered the real TextField and LookupField with an in-memory data source, then was deleted. Playwright readings: plain browser, 0 scan buttons; shell stub, 2 buttons; after scanning, the text input holds ASSET-0042 and the lookup dropdown opens with search ASSET-0042 listing only "ASSET-0042 Forklift"; a refused camera shows the toast "Could not scan / Camera permission denied". Files in the project thread: phone-plain-browser.png, phone-shell-buttons.png, phone-shell-after-scan.png, phone-shell-scan-failed.png.


Generated by Claude Code

The mobile shell's bridge now carries scan(opts) behind has('scan').
Type it as an optional member (older shells lack it) and call it through
callNative, so a caller gets unavailable, ok (text or null) or failed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUw36sPe8KXr9ANYMv
Inside the ObjectStack mobile shell, when the bridge announces scan, an
editable single-line text field and a lookup field show a scan button.
Text takes the decoded value through onChange; a lookup opens its search
with it (PeoplePicker gains initialSearch for the search-first variant).
A closed scanner changes nothing; a failure shows the error toast.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUw36sPe8KXr9ANYMv
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUw36sPe8KXr9ANYMv
@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tooling labels Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m

席位核对 · ACCEPT · 2026-11-02T16:45Z

Head-sha: c6569d8705bd5423f99ce34c6c618f7d907516ec

  • Shape: draft → main, Fixes #29378 in the body only. 22 files, +551 / −6 (557 lines ≤ 3000). No governed path: check-governed-merges --pr 29493 reports NOT governed (0 of 22 paths). Clause-②: yes and Metadata-protocol: none are line-initial in the body and the changeset, as claimed (6581080956, amended 6581140256). The changeset is patch for @object-ui/mobile, @object-ui/fields and @object-ui/i18n.
  • Contract review (seat's own, Clause-②: yes): PASS. Every addition is additive and optional:
    • @object-ui/mobile: 'scan' joins NativeHostMethod (used only inside nativeHost.ts; no exhaustive consumer elsewhere in the repo), ObjectStackNative.scan? is optional so older shells and typed stubs still conform, NativeScanOptions and nativeScan(opts?) are new exports through the existing callNative (unavailable / ok / failed), matching the shell contract in mobile#170.
    • @object-ui/fields: one optional PeoplePicker.initialSearch prop; ScanFieldButton / useFieldScanOffered stay internal (not in the package entry); TextField and LookupField gain no prop. New dependency edge fields → mobile (mobile depends only on types, sideEffects: false; grid, list, calendar and timeline already take it), no cycle.
    • @object-ui/i18n: fields.scan.button, fields.scan.failed in all ten locales plus FIELD_DEFAULTS. No metadata key: the affordance follows the bridge capability, so Metadata-protocol: none holds.
  • File surface: inside the claim and its amendment (nativeHost.ts + index, TextField.tsx, LookupField.tsx, PeoplePicker.tsx initialSearch only, the new internal ScanFieldButton.tsx, useFieldTranslation.ts, the locales, fields/package.json + lockfile for the edge, tests, changeset).
  • Read in the diff: the button shows only when useNativeHost() has scan as a function AND has('scan') is true (a throwing has reads false), re-rendering on a late bridge. Text: single-line, not password, not disabled; the decoded text goes through onChange untruncated, so validation judges it. Lookup: search-first variant opens PeoplePicker seeded via initialSearch (cleared on close; applied on open through a ref so a new setter never re-seeds over typing); classic and static variants open the popover with the code as search; hidden when the field is disabled, dependsOn is unmet, or the target offers no search. ok + text → handler; ok + null / empty or unavailable → nothing; failed → toast.error(fields.scan.failed, shell message). The button disables itself while a scan is open.
  • Accepted deviation: compact (grid-cell) lookups get no button.
  • Tests (dev): new ScanFieldButton.shellScan.test.tsx 13 / 13; mobile 46 / 46; mobile + i18n 104 files, fields 294 (+1 skipped), ui scripts/ 169; type-check 50 / 50. Ablation of the has('scan') gate reds exactly the two older-shell pins, restored to the HEAD blob. Derived gates 54, 53 exit 0; check:dual-build-cjs-loads NOT MEASURED locally (exit 3, needs a full build, scans root packages/ only); it scans root packages/ only, which this PR does not touch. Body gates 4 / 4.
  • Measured (dev): 390 px screenshots of a text and a lookup field with a stub bridge, on a temporary Console Vite preview entry (deleted): /mnt/project-files/issue-29378/.
  • Serial note: draft PR fix(mobile): the native bridge platform is an open set, and a newer bridge protocol reads as no bridge #29475 (Native host: make ObjectStackNative.platform an open set and pin it with an App-level test (follow-up to #29140) #29330) edits nativeHost.ts and its test in other hunks; whichever lands second merges main.
  • Gates (check-runs on this head): 48 success, 8 skipped, 0 failed or pending. mergeable_state is clean.
  • Landing: ordinary queue landing, pr_ready then automerge_enable. Fixes #29378 closes the card on merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 16:46
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 16:46
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit ec2f5d5 Nov 2, 2026
57 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-6pv9qk branch November 2, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console: offer a scan affordance on text and lookup fields when the mobile shell exposes scan()

1 participant