Repository navigation
Commit ec2f5d5
feat(fields): offer the mobile shell's code scanner on text and lookup fields (#29493)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbQnx983aXpxUvWnKCfqDKdq)_
Fixes #29378
Clause-②: yes
Metadata-protocol: none
## 维护者速读
- **改了什么**:在 ObjectStack 移动壳内(桥接 `has('scan')`
为真时),可编辑的单行文本字段与查找(lookup)字段旁出现一个「扫码」图标按钮;文本字段把解码结果经 `onChange`
写入,查找字段用解码结果打开搜索。普通浏览器、旧版壳、只读或禁用字段不显示按钮。
- **为什么改**:移动壳已上线 `scan()`(objectstack-ai/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).
<details><summary>Suites and type-check</summary>
- `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.
</details>
<details><summary>Derived gates</summary>
`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.
</details>
<details><summary>Phone-width screenshots (390 px, stub
bridge)</summary>
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`.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jUw36sPe8KXr9ANYMv)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 28d0dbb commit ec2f5d5
22 files changed
Lines changed: 551 additions & 6 deletions
File tree
- .changeset
- ui/packages
- fields
- src/widgets
- i18n/src/locales
- mobile/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| |||
310 | 311 | | |
311 | 312 | | |
312 | 313 | | |
| 314 | + | |
313 | 315 | | |
314 | 316 | | |
315 | 317 | | |
| |||
722 | 724 | | |
723 | 725 | | |
724 | 726 | | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
725 | 730 | | |
726 | 731 | | |
727 | 732 | | |
| |||
816 | 821 | | |
817 | 822 | | |
818 | 823 | | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
819 | 840 | | |
820 | 841 | | |
821 | 842 | | |
| |||
1694 | 1715 | | |
1695 | 1716 | | |
1696 | 1717 | | |
| 1718 | + | |
| 1719 | + | |
| 1720 | + | |
| 1721 | + | |
| 1722 | + | |
| 1723 | + | |
| 1724 | + | |
| 1725 | + | |
| 1726 | + | |
| 1727 | + | |
| 1728 | + | |
| 1729 | + | |
1697 | 1730 | | |
1698 | 1731 | | |
1699 | 1732 | | |
| |||
1753 | 1786 | | |
1754 | 1787 | | |
1755 | 1788 | | |
1756 | | - | |
| 1789 | + | |
1757 | 1790 | | |
1758 | 1791 | | |
1759 | 1792 | | |
1760 | 1793 | | |
1761 | 1794 | | |
1762 | 1795 | | |
1763 | 1796 | | |
1764 | | - | |
| 1797 | + | |
| 1798 | + | |
| 1799 | + | |
| 1800 | + | |
| 1801 | + | |
1765 | 1802 | | |
1766 | 1803 | | |
1767 | 1804 | | |
| |||
1777 | 1814 | | |
1778 | 1815 | | |
1779 | 1816 | | |
| 1817 | + | |
1780 | 1818 | | |
1781 | 1819 | | |
1782 | 1820 | | |
| |||
1964 | 2002 | | |
1965 | 2003 | | |
1966 | 2004 | | |
| 2005 | + | |
1967 | 2006 | | |
1968 | 2007 | | |
1969 | 2008 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
123 | 129 | | |
124 | 130 | | |
125 | 131 | | |
| |||
146 | 152 | | |
147 | 153 | | |
148 | 154 | | |
| 155 | + | |
149 | 156 | | |
150 | 157 | | |
151 | 158 | | |
| |||
240 | 247 | | |
241 | 248 | | |
242 | 249 | | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
243 | 262 | | |
244 | 263 | | |
245 | 264 | | |
| |||
0 commit comments