Skip to content

fix(plugin-list): 消费 PageComponentSchema.dataSource —— saved view 按名引用首次可用,写了 dataSource 不再把组件弄坏 (objectstack#5576) - #3929

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-os5576-datasource-view
Aug 9, 2026
Merged

fix(plugin-list): 消费 PageComponentSchema.dataSource —— saved view 按名引用首次可用,写了 dataSource 不再把组件弄坏 (objectstack#5576)#3929
yinlianghui merged 1 commit into
mainfrom
claude/issue-os5576-datasource-view

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes objectstack-ai/objectstack#5576

结论先说:「Couldn't load records」的真实机理不是 view 键被丢弃

issue 正文把两件事归到了一起。实测(单变量对照,见下)后它们是两个独立缺陷,必须分别修:

  1. 声明但丢弃(declared, not delivered) —— resolveElementDataSource 透传 filter/sort/limit、把 view 整个丢掉,且除测试外零调用方;渲染路径上也没有任何地方把 dataSource.object 映射到 list-view 真正读的 objectName。所以「按名字引用 saved view」这条能力在运行时不存在
  2. 写了 dataSource 反而坏掉 —— 与 view 无关,是同名撞车:SchemaRenderer 会把 schema 上所有非元数据键 spread 成 React props,而 dataSource 恰好正是宿主注入数据源适配器用的那个 prop 名。于是 spec 合法的 dataSource: { object, view } 这个纯 JSON 对象把适配器遮蔽掉,第一次 dataSource.find(…)dataSource.find is not a function,list-view 把它渲染成 "Couldn't load records"

实测原始读数(改前,packages/plugin-list 一次性探针,已删):

WITH-DS  → error: "Couldn’t load records…", errorKind: "network",
           find calls: [], getObjectSchema calls: []
           stderr: TypeError: dataSource.find is not a function
                   at fetchData (packages/plugin-list/src/ListView.tsx:1257)
NO-DS    → find calls: [["account", {"$select":["id","name"],"$top":100}]], error: null
WITH-DS 且不写 objectName → find calls: [], error: null(静默空表 —— 这才是 view 被丢弃那一半的症状)

也就是说:分诊预判的「view 键丢弃导致查询坏」不成立;view 被丢弃的表现是静默空表/全量,而 "Couldn't load records" 来自 prop 撞车。两条都在本 PR 修掉。

合成语义论证(spec 自己定了关键的一条)

ElementDataSourceSchema 的 describe 文本把唯一有歧义的键定死了 —— filter 是 "Additional filter criteria"。「additional」只能读成:view 已经限定的基础上限定,所以 view filter 与 binding filter 必须 and 合成,binding 只能收窄、不能放宽。这也是 AI 产出的元数据里最安全的方向:一个写错的 per-element filter 不可能把 saved view 排除掉的行重新暴露出来。

其余键 spec 没有 additional 之说,且都是单值(一个排序、一个上限),按分诊定的「view 提供基线、显式键覆盖」:

来源 规则
object binding(必填) binding 胜(spec: "Overrides page-level object context")
columns 只来自 view(binding 无此键) view
filter view + binding + 组件自身 三者 and 合成("additional")
sort view 或 binding binding 覆盖 view
limit view 的 pagination.pageSize 或 binding binding 覆盖 view
viewType 只来自 view view

组件自身写的键与 view 的关系单独说一句:view引用,组件上直接写的键比它指向的东西更具体,所以组件自身的键盖过 view(空的 columns: [] 视为「没写」—— 那是设计器对「未配置」的产物,而指定 view 的用意正是由 view 提供列)。dataSource.* 是作者在这一处写的,标准更高:它同时盖过 view 和组件自身的同名键。

view 名解析不到:报错,不是静默空表(语义已定并钉住)

解析不到时渲染一个配置错误面板(role="alert",列出该对象实际有哪些 view),并且不发任何查询。刻意不回退到对象默认 view:那会把一个拼写错误变成更宽的答案(整个对象 vs saved view 选出的行),而页面看上去还是好的 —— 这正是 AI 生成的元数据最容易藏住的一类错。姿态与 SchemaRenderer 既有的 "Unknown component type" 红框一致(同一类:作者写的元数据指向了不存在的东西),因此沿用英文文案而未新造 10 个 locale 的 i18n 键。

另有一条相邻但不同的事实要分开报:「该对象没有这个 view」与「这个数据源压根答不了 view 有哪些」是两件事,给两条不同的错误文案,不合并。

改了什么

  • @object-ui/react —— SchemaRendererdataSource 加入「schema 元数据、不作为 prop spread」名单(与 visibleWhen 同列)。消除撞车的根,对所有 data-bound block 一次生效;显式 React dataSource prop 不受影响(它走 ...props,最后 spread)。新增 useElementDataSource(schema, dataSource?),从对象定义的 listViews 与元数据 overlay 的 listViews() 两处取 saved view(与 app-shell ObjectView 同一优先级:overlay 覆盖 metadata)。
  • @object-ui/core —— 新增 element-data-source.ts:isElementDataSourceConfig(binding 与适配器的判别式)、collectSavedViewsresolveSavedViewcomposeElementDataSourceelementDataSourceViewNotFoundMessageresolveElementDataSource 不再丢弃 view:经可选的 DataFetcher.fetchViews 解析,解析不到就返回 error(而不是回退成全量)。resolveViewId 从 app-shell 移到 core(app-shell 原路径改为 re-export),使对象页与页面组件用同一个名字匹配器,而不是两份会漂移的实现。
  • @object-ui/plugin-list —— 新 ListViewBlock 取代原来的一行 ListViewRenderer,把 binding 映射到 ListView 真正读的 props;并保留一层判别式兜底(即使有宿主把 binding 塞进 dataSource prop,也不会被当成适配器)。

验证(两方向对照 + 反向验证)

packages/plugin-list/src/__tests__/ListView.elementDataSource.test.tsx 就是正文那份单变量复现:

  • dataSource: { object, view }find('account', …),$filter = view 的 filter、$orderby = view 的 sort、$select = view 的 columns,且 list-error-state 面板为 null;
  • 不带 dataSource → 行为逐字不变;
  • view 名解析不到 → 配置错误面板,find 零调用。

命令与读数:

pnpm exec vitest run packages/core/src/data-scope/ packages/react/ packages/plugin-list/ \
  packages/components/src/__tests__/page-variables.test.tsx packages/app-shell/src/utils/ \
  apps/console/src/__tests__/public-block-binding-reach.test.tsx --maxWorkers=2
→ Test Files  80 passed (80)     Tests  1152 passed (1152)

pnpm exec turbo run type-check --concurrency=2
→ Tasks:  78 successful, 78 total     (0 error TS)

反向验证:两处各自独立,方向不同,先预判后跑

两个缺陷是分开修的,所以反向验证也必须分开做,否则一处的红会掩盖另一处压根没被钉住。

RV-B —— 把 plugin-list/src/index.tsx 换回 origin/main(binding 消费撤掉,SchemaRenderer 的剥离保留)。
预判:view 系测试全红(objectName 未设 → 压根不发查询),但「不报 Couldn't load records」与「不带 dataSource 行为不变」两条仍绿 —— 前者靠的是剥离,不是 binding 消费。实测:

Test Files  1 failed | 1 passed (2)
     Tests  11 failed | 6 passed (17)
× renders the saved view's columns, filter and sort
× binds `object` with no `view` — no saved view needed
× AND-combines the binding filter with the view's, never replacing it
× lets the binding's own sort and limit override the view's
× lets columns authored on the component win over the view's
× reads a view from the metadata overlay (`dataSource.listViews`) too
× accepts the qualified spelling of a bare view key
× renders a configuration error naming the view, object and known views
× never falls back to an unfiltered query for the object
× reports it on an object with no saved views at all
× distinguishes "cannot answer" from "the view does not exist"

6 条通过的正是预判的那 6 条(2 条 plugin-list + 4 条 SchemaRenderer.dataSourceBinding)。

RV-A —— 把 packages/react/src/SchemaRenderer.tsx 换回 origin/main(撞车恢复,ListViewBlock 保留)。
预判(与「改回去就红」的直觉相反,故先写下来):plugin-list 的 13 条仍然全绿,因为 ListViewBlock 自己那层 isElementDataSourceConfig 兜底会拦住被塞进 prop 的 binding;转红的只有 SchemaRenderer.dataSourceBinding 里那条「不把 binding spread 成 prop」。实测读数与预判一致:

Test Files  1 failed | 1 passed (2)
     Tests  1 failed | 16 passed (17)
× does not spread the schema binding onto the component as `dataSource`

ListView.elementDataSource.test.tsx 的 13 条一条没红。这条反向读数本身就是一个结论:两层各自有独立的钉子,剥离(根)与判别式(兜底)不是同一件事的两种写法。

未做浏览器实证,如实说明

任务建议用 verify skill 做浏览器读数。没做,理由写清而不是补一个看起来像验证的东西:verify skill 的作用域是 metadata-admin designer 的 preview gallery(无后端),而本改动的关键路径恰恰是「从适配器取 saved view 并解析」—— 无后端时它必然走到「无法列出该对象的 saved view」那条分支,得到的读数不能支撑正向结论。真正的端到端实证需要一张 type: 'home' 页面上带 dataSource: { object, view } 的 list-view,而那份元数据在 objectstack 仓的 example app 里,超出本 PR 范围。替代证据是上面的真实单变量复现(含 TypeError 现场与 ListView.tsx:1257 行号)与两个方向的反向验证。

顺手记录的越界发现(未在本 PR 修)


Generated by Claude Code — session session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

…iew reference works, and writing the binding no longer breaks the block (objectstack#5576)

The spec declares a per-element data binding on every page component
(`dataSource: { object, view?, filter?, sort?, limit? }`) and objectui read none
of it on `list-view`. `ViewDataProvider.resolveElementDataSource` forwarded
`filter`/`sort`/`limit` and dropped `view` entirely, and had no caller outside
its own test; nothing mapped `object` onto the `objectName` a list actually
reads. "Reference a saved view by name" was published, validated and inert, so
every page that wanted a saved view's columns/filter/sort had to inline a second
copy of them.

Writing the binding also BROKE the block, for a reason unrelated to `view`:
`SchemaRenderer` spread the schema's `dataSource` metadata onto the component as
a React prop, and that is the prop name the host uses to inject the data-source
ADAPTER. The plain `{ object, view }` object shadowed the adapter, so the first
`dataSource.find(...)` threw `dataSource.find is not a function` and `list-view`
rendered "Couldn't load records" — a spec-compliant component failing next to
identical ones that omitted the binding.

- react: `SchemaRenderer` strips `dataSource` from the props it spreads (it is
  schema metadata, like `visibleWhen`); renderers read it off `schema`. An
  explicit React `dataSource` prop is unaffected. New
  `useElementDataSource(schema, dataSource?)` resolves a binding, fetching the
  named saved view from the object definition's `listViews` and the metadata
  overlay's `listViews()`.
- core: new `element-data-source` module — `isElementDataSourceConfig`,
  `collectSavedViews`, `resolveSavedView`, `composeElementDataSource`,
  `elementDataSourceViewNotFoundMessage`. `resolveElementDataSource` honours
  `view` through an optional `DataFetcher.fetchViews` and reports an
  unresolvable view as an error instead of returning every record.
  `resolveViewId` moved here from app-shell (re-exported there) so one matcher
  serves both the object page and a page component.
- plugin-list: `ListViewBlock` maps the binding onto the props `ListView` reads.
  `dataSource.*` keys are authoritative, view-supplied values are a baseline the
  component's own keys override, and `filter` AND-combines at every level (the
  spec calls the binding's filter "additional criteria"), so a binding can
  narrow a saved view but never widen it. A `view` name that does not resolve
  renders a configuration error naming the object's actual views and issues no
  query — no fall back to the default view, because that turns a typo into a
  silently wider answer.

Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 9, 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 Aug 9, 2026 6:42am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation package: core package: react plugin tests labels Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CPxVts-3.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
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.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
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.35KB 1.07KB
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) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 482.53KB 106.37KB
core (index.js) 3.00KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 231.00KB 56.76KB
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.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
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) 6.96KB 1.98KB
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) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
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.41KB 1.44KB
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.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 117.21KB 30.27KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 236.63KB 59.02KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.63KB 49.66KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 109.67KB 26.55KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
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.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 20.15KB 6.72KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
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) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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

Copy link
Copy Markdown
Collaborator Author

验收通过(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)objectstack-ai/objectstack#5576(v17,路线 1)

实物核验:base 5e524950d → head 8fc36c2f0,单提交 18 文件(触 docs 为 guide 页,非 releases/ 禁区)。CI 亲读终态:20 检查全 completed、0 失败。

验收要点:

  • 机理更正是本单最有价值的产出:「Couldn't load records」真因不是 view 键丢弃,而是 SchemaRendererdataSource 元数据 spread 成 React prop、遮蔽宿主注入的适配器(dataSource.find is not a function,探针实测栈顶 ListView.tsx:1257);view 丢弃的真实症状是静默空表。修法分层正确:react 侧剥离名单除根(对所有 data-bound block 一次生效,显式 React prop 不受影响)、core 侧纯函数模块承载判别式与合成、plugin-list 的 ListViewBlock 接线。
  • 合成语义从 spec 自述推导而非发明:filter 按 describe 的 "Additional" 语义 AND 合成(binding 只能收窄不能放宽 —— 对 AI 作者笔误是安全方向);sort/limit 显式覆盖 view 基线;viewType 随行(命名 kanban view 渲染成 grid 是静默错答案)。view 名解析不到渲染 role=alert 错误面板并列出实际可用 view、零查询、拒绝回退默认 —— declared ≠ delivered 的反面被正确堵死。
  • 适配器/元数据判别式(find 函数存在即适配器)+ 两轮方向相反的反向验证(RV-A 预判与直觉相反且吻合)扎实。
  • 验收线达成:加/去两方向单变量复现均有钉子,写 binding 不再弄坏组件。

范围外 finding objectstack#6953(record_picker 等其余 block 的逐 block 接线,Blocked-by 本单)立单规范,冻结期不派。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 06:51
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit e06810e Aug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-os5576-datasource-view branch August 9, 2026 06:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants