Skip to content

fix(flow-designer): read approver value sources off the schema instead of mirroring them (framework#3508 follow-up) - #2910

Merged
os-zhuang merged 1 commit into
mainfrom
claude/approver-xref-sources
Jul 28, 2026
Merged

fix(flow-designer): read approver value sources off the schema instead of mirroring them (framework#3508 follow-up)#2910
os-zhuang merged 1 commit into
mainfrom
claude/approver-xref-sources

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

配套 framework objectstack#3817(spec 侧新增 xRef.sources)。需与之一起看:本 PR 消费那份注解。

问题

审批人 Value 选择器自己决定候选从哪来,靠的是一份手工镜像 spec APPROVER_VALUE_BINDINGS 的本地常量 KIND_TO_RECORD_LOOKUP

这份镜像正是 framework#3508 得以发生的原因:xRef.map 只说了渲染哪种 picker'team'),没说这个 picker 背后是什么,于是本包只能自己挑一个数据源 —— 挑成了元数据注册表 GET /api/v1/meta/:type,而它列不出 sys_user / sys_team / sys_business_unit / sys_position记录。候选恒空,控件退化成手敲 id 的文本框。

镜像修好了,但镜像本身还在:下一个新增的审批人类型照样可能悄悄落后。

改动

spec 现在把数据契约作为 xRef.sources 发出来(逐类型:{ source: 'data', object, valueField }、闭合枚举内联、或非 picker 标记)。

  • json-schema-to-fields 把它读进 FlowReferenceSpec.sources逐条校验:无法驱动 picker 的条目直接丢弃(比如 data 源缺 valueField —— 那会查出一个什么都问不到的 picker,正是本次要终结的失败)。
  • 新增 recordLookupFor()服务端发布的 source 优先,本地表退为兜底。
  • source 按判别值(审批人 type)取,不是按 picker kind —— roleorg_membership_level 共用一个 kind,但在上游是两个枚举成员。

没有移走的是表现层:显示哪个字段、要不要开 PeoplePicker、行副标题写什么,仍是本包的决定。spec 出数据契约,不出长相。

两处刻意的保守:

  1. 老服务端(没有该注解)走本地表,行为不变;
  2. 服务端给了 data source 但本包没有对应表现条目时,仍然渲染 lookup(标签退回提交列),而不是把一个明明可解析的引用降级成自由文本。

顺带纠正一处误导注释

flow-node-config.ts 里审批人 type 的 options 是离线兜底 —— FlowNodeInspector 渲染的是 serverFields ?? fieldsForNodeType(...),接了真后端就走引擎发布的 configSchema,这份列表根本不读。

所以 #2834 只写在那里的「间接绑定优先、user 垫底」从未生效:实测线上下拉是 spec 枚举顺序,User 排第一,正好相反。顺序已改到 spec 的 ApproverType 枚举(objectstack#3817),注释也改成说清哪份才算数。

测试

  • 新增 FlowReferenceField.sources.test.tsx(9 例):发布的 source 胜出本地表;缺失时回退;enum/auto/unsupported 不走记录路径;表现层仍来自本地;判别值 keying 能区分 roleorg_membership_level;无表现条目的 data source 仍出 lookup。
  • json-schema-to-fields.test.ts +2:sources 透传且丢弃不可用条目;老服务端不产生该字段。
  • app-shell 全量 800 通过(118 文件);pnpm --filter @object-ui/app-shell type-check 干净。

仓库全量 pnpm type-check@object-ui/plugin-map 有一处既存失败(maplibre-gl@6 无 default export),与本 PR 无关;它会挡住 turbo 后续任务,故 app-shell 的类型检查是在单独把依赖 build 好后跑的。

依赖顺序

本 PR 对老服务端安全(sources 缺失即回退),可先合;但要真正生效,需要 objectstack#3817 发布后的后端提供 xRef.sources

🤖 Generated with Claude Code

https://claude.ai/code/session_01BVRVgSvmyCwmDfTmKmCZoH


Generated by Claude Code

…d of mirroring them (framework#3508 follow-up)

The approver Value picker decided WHERE its candidates live from a local table
hand-mirrored from the spec's `APPROVER_VALUE_BINDINGS`. That mirror is what
made framework#3508 possible: `xRef.map` names a picker KIND and nothing more,
so this package had to choose a data source itself — and chose the metadata
REGISTRY, which lists no `sys_user` / `sys_team` / `sys_business_unit` /
`sys_position` rows. Candidates were always empty and the control degraded to a
raw-id text box.

The spec now publishes the contract as `xRef.sources`. `json-schema-to-fields`
carries it through — validating each entry and dropping any that could not
drive a picker (a `data` source with no committed column would query nothing,
which is the failure this ends) — and the new `recordLookupFor()` prefers it
over the local table. A new approver type can no longer leave a stale mirror
here.

Presentation deliberately does NOT move: display field, people-picker opt-in
and row subtitles stay local, so the spec ships the data contract and not the
look. The local table stays as the fallback for a server that predates the
annotation, and a `data` source for a kind with no local presentation entry
still renders a lookup labelled by its committed column rather than degrading a
resolvable reference to free text.

Sources are keyed by the DISCRIMINATOR, not the picker kind: `role` and
`org_membership_level` share one kind but are separate enum members upstream.

Also corrects the approver `type` options comment in `flow-node-config.ts`.
That list is the OFFLINE fallback — `FlowNodeInspector` renders
`serverFields ?? fieldsForNodeType(...)`, so against a real backend the
engine-published configSchema wins — which is why the "indirect bindings lead,
`user` last" order stated only there never reached the live picker (it followed
the spec enum, `user` first). The ordering now lives in the spec's
`ApproverType` enum; the comment says which list is authoritative.

Tests: 9 new cases (published source wins; fallback when absent; non-data
sources stay off the record path; presentation stays local; discriminator
keying) plus 2 for the schema reader. app-shell 800 pass, type-check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVRVgSvmyCwmDfTmKmCZoH
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 28, 2026 7:45am

Request Review

@github-actions github-actions Bot added the tests label Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 27.9 KB 350 KB
Entry file index-Cum6UE0x.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.25KB 1.01KB
auth (org-roles.js) 6.72KB 2.85KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
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) 449.62KB 97.73KB
core (index.js) 2.12KB 0.77KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 127.78KB 32.15KB
fields (index.js) 218.37KB 53.54KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 10.67KB
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.74KB
mobile (index.js) 1.50KB 0.62KB
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) 4.42KB 1.27KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 1.77KB 0.77KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 6.84KB 2.42KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.00KB 1.23KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 57.26KB 16.24KB
plugin-chatbot (index.js) 179.93KB 42.67KB
plugin-dashboard (index.js) 109.60KB 28.33KB
plugin-designer (index.js) 210.56KB 42.56KB
plugin-detail (index.js) 214.86KB 52.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 103.47KB 25.10KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 179.25KB 46.97KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 98.67KB 23.35KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 37.07KB 9.81KB
plugin-timeline (index.js) 25.03KB 7.11KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.68KB 20.85KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.55KB 0.67KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 3.19KB 1.38KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 18.70KB 6.09KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.00KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 2.16KB 0.94KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 1.86KB 0.91KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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 (spec-report.js) 5.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.75KB 0.46KB
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

@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 07:49
@os-zhuang
os-zhuang merged commit eea4391 into main Jul 28, 2026
14 checks passed
@os-zhuang
os-zhuang deleted the claude/approver-xref-sources branch July 28, 2026 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants