Repository navigation
feat(console): take photos, open attachments and share records through the mobile shell - #29803
Conversation
…ith typed wrappers Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSqiKZMCzGXQK5xzUq
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSqiKZMCzGXQK5xzUq
…ord Share through the shell share Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSqiKZMCzGXQK5xzUq
…onsole Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSqiKZMCzGXQK5xzUq
… file helpers Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSqiKZMCzGXQK5xzUq
|
Contract-review-claim: session_01QsR2jV5sBSzCYNEDgpYLJw · head b37e312 · 2026-11-03T05:40Z Generated by Claude Code |
|
director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw Contract reviewServed-tier: Read-only; nothing built or run. Card #29735 (mobile#147 ruling letter A: the Console fetches files with its own session; the shell never touches the session). ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
No must-fix. No should-fix. Implemented-by: VERDICT: PASS Generated by Claude Code |
…-6oh3sk Keeps both sides of the bridge: the app-lock members and wrappers from main beside pickImage, openFile and share. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSqiKZMCzGXQK5xzUq
|
Contract-review-claim: session_01QsR2jV5sBSzCYNEDgpYLJw · head 09666e3 · 2026-11-03T06:17Z Generated by Claude Code |
|
director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw Contract reviewServed-tier: Re-review for a conflict resolution, not a regen. Read-only; nothing built or run. ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
No must-fix. No should-fix. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m 席位核对 · ACCEPT · 2026-11-03T06:17ZHead-sha:
|
domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m
Requested by Steedos · project thread
Fixes #29735
Clause-②: yes
Metadata-protocol: none
Claim: comment 6595193307 (branch
claude/project-thread-6oh3sk); file surface amended in 6595245493 (seat ruling: record share only, never pass the signed URL to the shell), plusui/packages/mobile/README.mdon the seat's word.维护者速读
pickImage()),记录附件点开时由 Console 用自己的会话取回文件字节交给外壳的openFile()预览,记录页「分享」走外壳的share()调起系统分享面板。普通浏览器和旧版外壳行为完全不变。has()),不满足就走原来的路径;拍到的照片走字段原有的上传管线(同样的大小校验、同样存sys_file引用)。附件超过 8 MiB、取字节失败或外壳拒收(bad_args)都回到原来的打开方式。签名下载链接是凭证,从不交给外壳。回滚即 revert 本 PR。ui/packages/app-shell/src/views/RecordAttachmentsPanel.tsx的openInNativeShell(取字节、8 MiB 判断、回退);②ui/packages/fields/src/widgets/FileField.tsx/ImageField.tsx里照片如何进入原上传管线;③ui/packages/mobile/src/nativeHost.ts新增的可选成员与包装函数(公开面扩展,需契约复核)。cd ui && pnpm exec vitest run packages/fields/src/widgets/nativeImagePick.shellPhoto.test.tsx packages/app-shell/src/views/__tests__/RecordAttachmentsPanel.test.tsx packages/app-shell/src/views/RecordDetailView.favoritesEntry-12427.test.tsx packages/mobile/src/__tests__/nativeHost.test.tsx,预期 4 个文件全绿(135 个用例)。@object-ui/mobile公开面扩展的契约复核由席位转总监)。What changes
The ObjectStack mobile shell offers three optional bridge methods for files (objectstack-ai/mobile
docs/HYBRID-SHELL.md, "Files", shell side mobile#180), each feature-detected withhas(). The Console now calls them. A plain browser and an older shell take exactly today's path.@object-ui/mobile,nativeHost.ts).ObjectStackNativegains optionalpickImage?,openFile?andshare?(an older shell lacks them), the method union gains the three names, and the typed wrappersnativePickImage,nativeOpenFile,nativeSharefollow thescan/showConnectionSwitcherprecedent (feature-detect throughnativeHostCan, thenhost.x!(); resolveunavailable | ok | failed, never throw). New typesNativePickImageOptions,NativeFile,NativeShareContent; the bridge limitNATIVE_FILE_MAX_BYTES(8 MiB, the shell'sMAX_FILE_BYTES); helpersfileFromNativeFile(bridge file to an uploadableFile) andnativeFileFromBlob(bytes to padded base64 with nodata:prefix and a parameter-free MIME type). All exported from the package entry: a public widening, so a contract review is owed.@object-ui/fields). In a shell announcingpickImage,FileField(when the field accepts images) routes its existing camera button throughpickImage({ camera: true })and adds "Choose photo" (pickImage({ camera: false }));capture: falsestill hides the camera.ImageFieldadds "Take photo" and "Choose photo" under its upload button. The picked image becomes aFileand enters the SAME pipeline an input pick does:FileField'sprocessFiles, andImageField's input handler body lifted intouploadPicked, which both the input and the shell call. No second upload route. Cancel (null) uploads nothing; a shell failure shows the error toast with the shell's message (fields.file.photoFailed). Internal helperwidgets/nativeImagePick.ts, not exported. The gridFileCellandAvatarFieldare not touched.@object-ui/app-shell,RecordAttachmentsPanel). The row's Download tap still fetches the signed URL with the Console's own session. In a shell announcingopenFile, it then fetches the bytes and hands only the bytes toopenFile. A row over 8 MiB (judged before any byte is fetched, and again on the fetched size), a failed byte fetch (for example a cross-origin storage URL without CORS), or any non-okanswer (bad_argsincluded) keeps today'swindow.open.@object-ui/app-shell,RecordDetailViewsys_share). In a shell announcingshare, the record header's Share callsshare({ url: location.href, text: document.title }). A refusal or failure falls back to today'snavigator.shareor clipboard copy. Only the record URL crosses the bridge.fields.file.choosePhotoandfields.file.photoFailedin the ten locale packs.ui/packages/mobile/README.md: the typed-wrapper line names the three new wrappers, and one entry namesNATIVE_FILE_MAX_BYTESand the two file helpers (added to the amended file list on the seat's word: doc follows export).Tests
packages/mobile/src/__tests__/nativeHost.test.tsx: the fake bridge carries the three methods; each wrapper resolvesunavailablewith no bridge, whenhas()says no, and when the method is missing;pickImageforwards options and resolves the file ornull;openFileforwards the file and resolvesfailed/bad_argson refusal;shareforwards url and text; the 8 MiB constant; base64 decode and encode helpers.packages/fields/src/widgets/nativeImagePick.shellPhoto.test.tsx(new, 12 tests): buttons only whenhas('pickImage'); camera and library both upload the pickedFile(name, type, size checked) through the field'sUploadProvideradapter andonChangegets thesys_fileid; cancel uploads nothing; failure toasts and uploads nothing;capture: false; a non-imageaccept; the picked image meets the samemaxSizecheck. CONTROL: no shell and an older shell draw no shell button.packages/app-shell/src/views/__tests__/RecordAttachmentsPanel.test.tsx, new top-level describe (6 tests):openFilegets exactly{ base64, name, mime }, the signed-URL token appears in no shell call,openExternal/shareuntouched, no tab; over 8 MiB by row size (no byte fetch) and by fetched size;bad_args; failed byte fetch; CONTROL older shell. The plain-browser control is the existing signed-URL download describe, unchanged and green.packages/app-shell/src/views/RecordDetailView.favoritesEntry-12427.test.tsx, new top-level describe (4 tests): Share callssharewith exactly the record URL and title and copies nothing; refusal falls back to the clipboard; CONTROL older shell and plain browser copy as today.scripts/ablation-replace.mjs(blob == HEAD,git diff HEADempty): bridgeshareforwards{}(1 red);FileFielddrops the picked files (2 red);ImageFielddrops the picked files (2 red); the panel skipsopenInNativeShell(2 red); Share ignores the shell'sok(1 red).Acceptance notes
/storage/files/:idlink needs the Console's Bearer session (dead for a recipient), and the signed URL is a credential.plugin-detail'sDetailViewhas its ownsys_share; it is not on the Console record page and is unchanged.FileValueAffordance), the gridFileCellandAvatarFieldkeep today's behaviour; the contract names attachments and image/attachment fields.openFilepasses the row's file name as is; a name the shell refuses (bad_args) takes the browser path.Verification
Final head
09666e32: mergedorigin/mainb62463f after PR #29798 landed the app-lock bridge members in the same files. The conflicts innativeHost.ts(method union, interface members, wrappers),index.ts, thenativeHost.test.tsxfake andui/packages/mobile/README.mdwere resolved as a union. Both sides' members, wrappers, exports and keys are kept, and none is dropped.en.tsmerged on its own. After the merge, the@object-ui/app-shellclosure was rebuilt together with formula, sdui-parser, lint, client and client-react (66 tasks, exit 0).09666e32:turbo run type-checkfor@object-ui/mobile,@object-ui/fields,@object-ui/app-shelland@object-ui/i18n: 68/68 tasks, exit 0.09666e32(locked, 2 workers): the native-shell set (git grep -lof@object-ui/mobileorObjectStackNativein tests, which now includes the app-lock tests), every test namingFileField,ImageFieldorRecordAttachmentsPanel, the four new or touched test files, andpackages/i18n/: 150 files, 2825 passed, 13 skipped, 0 failed.RecordDetailView*.test.tsxat3d7a69ca: 99 files, 619 tests green. Neither main merge touchedRecordDetailView's share entry.3d7a69ca).09666e32:dispatch-gates --commandsderived 35, all 35 ran, and all exited 0 (this includespnpm --dir ui exec vitest run scripts/,check:type-check-debt,check:published-files,check:nul-bytes,check-sdui-manifest,check-changeset-no-major,check-adr-0087-registration, andcheck-closing-keyword-parityagainst this body).dispatch-gates --ran: 35 run, 0 NOT-MEASURED. The same 35 also exited 0 atb37e3124.b37e3124. This merge adds no line to this PR's own diff.c413ef3c.Generated by Claude Code