Skip to content

feat(studio): customize a read-only package's dataset in the All metadata area, see what uses it before publishing, and read a refused publish at the dataset - #30224

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-30064-studio-dataset-customize
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-30064-studio-dataset-customize

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
Fixes #30064

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:Studio「全部元数据」里,只读基础包(如 CRM)的数据集不再只能看:打开后是数据集设计器加实时预览,标题栏有「定制」。第一次保存把整个数据集复制进配对包 BASE.custom 并声明 overrides: { package, basedOn },顶栏「发布」生效,覆盖标记里的「恢复软件包原版」撤销。编辑器上方列出用到这个数据集的仪表盘、报表等(来自 引用关系 读取),发布被拒时原因按控制台语言显示在数据集编辑器里,并定位到具体度量和输入项。
  • 为什么改:客户对「在途商机总额」的口径和厂商不同,只能改仪表盘、改不了下面的数字;而一个数据集同时喂给所有基于它的仪表盘和报表,发布前必须让人看到会影响哪些。
  • 风险与回滚:只改 ui(@object-ui/app-shell、@object-ui/data-objectstack、@object-ui/i18n),没有服务端或 spec 改动。可写包里的数据集仍走原来的通用条目页。顺带修了一个已发布缺陷:MetadataClient.references() 读的是服务端从不返回的 items 键,Setup 的「引用关系」页和 Studio 条目工具对每个条目都显示「未找到引用,可以安全删除」;现在按声明的 { references: [...] } 解析,读不懂的答复算读取失败。回滚 = revert 本 PR。
  • 先看:① ui/packages/data-objectstack/src/metadata-client.ts 的 references()(引用读取的修复,影响 Setup 引用页);② ui/packages/app-shell/src/views/studio-design/StudioBundleItemEditor.tsx 的 StudioBundleUsedBy 与设计器挂载(Designer 分支、blocking 拦截自动保存);③ metadataError.ts 的 datasetMemberLocator。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/StudioBundleItemEditor.customize-28569.test.tsx packages/data-objectstack/src/metadata-client.references-30064.test.ts,预期 16 例全绿(其中 10 例是本卡新增)。
  • 需要你做的:无,席位落地。

What changes

  1. The door. metadataArea.ts gains AREA_CUSTOMIZE_EDITOR_TYPES (dataset): on a READ-ONLY package the All metadata area opens a dataset in its form editor (StudioBundleItemEditor), the same editor that customizes an email template or a translation. areaItemOpensFormEditor takes the area's readOnly gate; on a writable package a dataset still opens the generic item page, as on Setup's. dataset is overridable in OVERRIDE_TYPE_CLASSIFICATION ( dataset: listed re-checked on the base).
  2. The designer in that editor. A type with a registered default inspector is edited in it rather than in the generic form: the dataset gets DatasetDefaultInspector, beside its registered DatasetPreview. Its shallow patch is applied to the buffer, and a blocking issue it reports (a dimension with no field) holds the autosave, as the generic page does. Email templates and translations register none, so they are unchanged.
  3. Customize, saved whole into the pair, with Restore. Nothing new: the editor's existing route (feat(studio): customize a read-only package's view, dashboard or report into its <base>.custom pair, with an override badge and "Restore package original" #28711's controls, Studio: email template and translation editors in the All metadata area — a read-only base's template or bundle is customized as an override in <base>.custom #28569's editor). The first saved edit asks the ensure door once, then writes the whole dataset into BASE.custom declaring overrides: { package, basedOn } (overrideDraftBody); the header's Publish publishes the pair; the override mark's popover offers "Restore package original". A note before the first save says the whole dataset is copied.
  4. What uses it, before publish. While the dataset is customized, the editor lists every item that names it, one entry per item, each opening in the area, from the existing references read (client.references, the item tools' 引用关系). A failed read says it failed and offers Retry; it never reads as "nothing uses it". No new server read.
  5. The references read reads its declared answer. GET /meta/:type/:name/references serves { references: [{ type, name, label?, path, kind }] } bare (FindReferencesToMetaResponseSchema); MetadataClient.references() read data.items, which the route never serves, so it returned [] for every item and both panels said "Safe to delete". It now parses the declared answer and maps each element to MetadataReference (fromType, fromName, new optional fromLabel and kind); an answer the declaration refuses throws. Measured on the CRM example before the fix: the route answered the Pipeline Dashboard for opportunity_metrics, and the reader dropped it.
  6. A refused publish, at the item. publishLocator gains datasetMemberLocator: an issue under measures.N or dimensions.N is placed on that member by its label and on the designer's input (Not published. Check Aggregate of the measure "Total Amount": Details say what to change. / 「未发布。请检查度量「总金额」的「聚合方式」」). The surface hands the area its kept refusals (publishRefusalFor), and the editor shows the one for its item, as the Pending changes entry does. The server's text stays under Details, verbatim (Studio publish refusals: shown only as a ~4 s toast with engine-internal text; the Pending changes panel keeps no refusal; "Show me" does not focus the offending input and the dashboard/report refusal strip has no "Show me" #26730's rule).
  7. Marks, the 仅看定制 count and the tile. No code: they read the customization read by type, and it lists the dataset override. Measured in the browser (below).

Why field: expected_revenue was refused: expected_revenue is a formula field, and the authoring gate's measure-aggregate-field-type-refused refuses sum/avg over a formula field (virtual, no stored column; its hint: store the value as a number field and aggregate that). Acceptance 6: no server or spec change was needed.

Tests

  • StudioBundleItemEditor.customize-28569.test.tsx (the editor's shared harness), new describe "Customize a read-only base's dataset (objectstack#30064)", 6 cases: designer read-only before Customize, the whole-dataset note, the used-by list deduplicated per item; Customize + an edit write the whole dataset into the pair, declared, base untouched; a blocking report holds the edit unsent; a failed references read says so and Retry reads again; an empty answer says nothing uses it; a kept publish refusal shows at the item with Details.
  • metadata-client.references-30064.test.ts (new, 4 cases): the fixture is the declared shape; mapping with and without a label; the honest empty answer; an off-contract answer throws.
  • metadataError.publishRefusal-26730.test.ts, new describe (2 cases): the measured formula refusal placed on the measure's Aggregate in zh, engine text kept out of the sentence; a dimension named by its name; a path under no member left unplaced.
  • metadataArea.test.ts, new describe (3 cases): dataset opens the editor only on a read-only package; the editor's exceptions kept; email template and translation unchanged.
Test runs (head aefff1f unless noted)
  • pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/ → Test Files 324 passed (324) · Tests 2412 passed (2412); lock VERDICT command-exit 0.
  • pnpm --dir ui exec vitest run packages/data-objectstack/ packages/i18n/ packages/app-shell/src/views/item-tools/ packages/app-shell/src/views/metadata-admin/ResourceEditPage.references packages/app-shell/src/views/metadata-admin/inspectors/DatasetDefaultInspector on 66709ce → 1 red: studio-namespace.test.ts placeholder parity (usedByOne en had no {count}); fixed in aefff1f, then pnpm --dir ui exec vitest run packages/i18n/ → Test Files 95 passed (95) · Tests 1443 passed | 13 skipped.
  • pnpm --filter @object-ui/data-objectstack --filter @object-ui/i18n --filter @object-ui/app-shell run type-check → all three Done, lock VERDICT command-exit 0 (after building the app-shell dependency closure with declarations).
  • Ablation 1 (node scripts/ablation-replace.mjs, committed head): blocked: !editable || restoring || blocking > 0, → blocked: !editable || restoring, — anchor 1 → 0, blob 8500a768 → 53bc7c49; "holds an edit the designer reports as blocking" ×, Tests 1 failed; restored blob == HEAD, git diff HEAD empty.
  • Ablation 2: fromType: ref.type, → fromType: ref.kind, — anchor 1 → 0; "maps each served reference …" ×, Tests 1 failed | 3 passed; restored blob == HEAD.
  • Both ablated files import their subject from source (./metadata-client, ./StudioBundleItemEditor), not a built dist/.

Acceptance notes

  • Published surface (Clause-② yes, widening). MetadataReference, exported from the @object-ui/data-objectstack entry, gains two optional fields: MetadataReference.fromLabel (the referring item's label, when served) and MetadataReference.kind (the served reference kind). The same diff narrows what MetadataClient.references() accepts: a 200 answer FindReferencesToMetaResponseSchema refuses now throws instead of resolving [] (a non-2xx answer already threw). Every caller in this repository reads it through useReferencesScan (ui/packages/app-shell/src/views/item-tools/ReferencesPanel.tsx), whose catch sets the error arm, and none calls it bare: Setup's item page (ResourceEditPage.tsx, the References sheet) and Studio's item tools (StudioItemTools.tsx, the References sheet) both render ReferencesPanel's error arm on the throw, the refsErrorTitle / refsErrorDesc sentences (the check did not finish, nothing said about deleting), the cause, and Retry, with no count badge; the new used-by list (StudioBundleItemEditor.tsx, StudioBundleUsedBy) shows usedByError with Retry. Callers in other repositories were not read.
  • In-place fix, named: the references read (item 5 above). It is the read this card was told to reuse; the card's Acceptance 2 cannot be met with it returning []. Evidence: the route's answer for opportunity_metrics on the CRM example was {"references":[{"type":"dashboard","name":"pipeline_dashboard","label":"Pipeline Dashboard","path":"widgets[].dataset","kind":"dashboard dataset"}]}, and the reader took data?.items ?? []. The same fix changes what Setup's References tab and Studio's item tools show for every type.
  • The dataset designer keeps its own count line ("Bound by 0 reports · 1 dashboard"), read by a client-side list scan (useDatasetUsage); the new list is the references read. Two reads of one fact sit on the page; they agreed in the browser check. Not changed here.
  • A refused publish's reason (here: a formula field stores no value) stays in the server's English under Details; the Console sentence names the measure and its input. A designer sentence per rule would be a separate decision.
  • Seed data differs from the card's: the CRM example's 在途商机总额 reads 710,000 here, not ¥814,400.

Verification

Browser check on the CRM example (objectstack dev --fresh, own port) with the Console's Vite dev server on its own port, Chromium, locale zh-CN. Screenshots are in the dev's scratchpad, not uploaded.

Steps and readings
  1. /studio/com.example.crm/metadata/dataset/opportunity_metrics: designer read-only, 定制 in the header, the whole-dataset note, 「发布后,这里的修改会影响基于此数据集的 1 项:Pipeline Dashboard 仪表板」.
  2. 定制 → measure 2 (total_amount) 聚合方式 求和 → 平均值: status 未发布草稿, mark 覆盖 CRM (minimal example); the pair's draft read back with overrides: {"package":"com.example.crm","basedOn":"4.0.0"} and all three measures.
  3. 发布 → 全部发布: GET /packages/com.example.crm/customizations lists {"type":"dataset","name":"opportunity_metrics","basedOn":"4.0.0"}. Dashboard 在途商机总额 710,000 → ¥177,500.
  4. Directory tile 数据集: 已定制 1; dataset list: 仅看定制(1), row mark 已定制(CRM (minimal example)).
  5. Measure 2 字段 金额 → 预期收入 (expected_revenue, formula), 发布: Pending changes entry and the dataset editor both read 「未发布。请检查度量「总金额」的「聚合方式」:详情中说明了需要修改的内容。」, Details holds the gate's line.
  6. 覆盖标记 → 恢复软件包原版 → confirm: mark gone, 定制 back, customizations read empty; dashboard 在途商机总额 ¥710,000.
Gates (local, head aefff1f)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 35 commands; every one exited 0 (check-adr-0087-registration, check-changeset-no-major, check-closing-keyword-parity, check-comment-mask-corpus, check-empty-changeset, check-sdui-manifest and --self-test, check:nul-bytes, check:published-files, check:type-check-coverage, check:type-check-debt, check:undeclared-dep-imports, check:test-source-alias, check:cross-package-test-inputs, pnpm --dir ui exec vitest run scripts/ (Test Files 170 passed | 2 skipped) and the rest). --ran reconciliation: 35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED (a DERIVED zero — all 35 recorded an exit code and none of them is 3). ui/scripts/check-new-cross-file-line-citations.mjs: 0 new citation(s). The type-check lanes and the wide-population families are CI's.


Generated by Claude Code

…etadata area, with what uses it and its publish refusal at the item (#30064)

Claude-Session: https://claude.ai/code/session_01QSeKYnE9YNkSZen7sijLA3
Co-authored-by: Claude <noreply@anthropic.com>
… used-by list, its publish refusal, and the references read (#30064)

Claude-Session: https://claude.ai/code/session_01QSeKYnE9YNkSZen7sijLA3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
…aReference fields and the references() narrowing (#30064)

Claude-Session: https://claude.ai/code/session_01QSeKYnE9YNkSZen7sijLA3
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3

维护者速读

  • 改了什么:Studio 里只读安装包的数据集可以「定制」:整份复制进 <base>.custom 并声明覆盖,可「恢复原版」;发布前列出哪些仪表盘、报表在用它;发布被拒时原因落在对应的度量/维度上,用 Console 语言显示。顺带修了引用读取一直读错字段、导致所有元数据项都显示「可安全删除」的缺陷。
  • 为什么改:你的「全部立,继续测试验证」;卡上 Acceptance 1–6。
  • 风险与回滚:只动前端,不动服务端与 spec;复用既有视图/仪表盘定制路径。回滚即撤本 PR。
  • 先看:studio-design/StudioBundleItemEditor.tsx(定制、引用列表、拒绝落位)、data-objectstack/src/metadata-client.ts 的 references()。
  • 两分钟验证:示例 CRM → Studio → 全部元数据 → 数据集 opportunity_metrics → 定制,把「总金额」的聚合改成平均 → 发布,仪表盘「在途商机总额」变化;恢复原版后复原。
  • 需要你做的:无,席位落地。

席位意见:ACCEPT。dev 浏览器实测 710,000 → 177,500 → 恢复 710,000。

席位核对

  • Verdict: ACCEPT · Head-sha: 9ba82b772b18f1d9a79df38a3b6173892f586dec · 2026-11-05T02:49Z
  • 形态:draft → main,Fixes #30064 唯一闭合关键词。
  • 前提实测:dataset 按 OVERRIDE_TYPE_CLASSIFICATION 可覆盖;隔离复核确认 doSave → overrideDraftBody 与 restoreOriginal 未改,无第二写路径。
  • 门禁:ci-failure --pr 30224 GREEN,55 项、0 失败;check-expected-skips OK(13/13);check-governed-merges:非受管,+682/−28。
  • Clause-②:yes (widening)(认领 6646368627 已追加式更正):MetadataReference 新增可选 fromLabel/kind。契约复审档复核 PASS(隔离 CONTRACT_REVIEW_TIER 子代理,读 head aefff1f6,Local-runs: none):references() 收窄只针对从未声明过的回包(服务端 packages/rest/src/meta-item-read-routes.ts:280 返回 {references:[…]},与 FindReferencesToMetaResponseSchema 一致,空结果可解析);唯一调用点 ReferencesPanel.tsx:69 有 catch,错误态带重试;新增字段为纯增量。当前 head 9ba82b77 相对复核 head 只改 changeset 文本。
  • 接受的偏差:本地未跑 app-shell 全量(CI Test UI 绿覆盖);PR 正文按 ui/AGENTS.md Migrate documentation site to Fumadocs with monorepo structure and shared content #11 用文件+符号而非行号引用。
  • 范围外去向:引用读取缺陷 → 本 PR 内修复,载于 Acceptance notes;数据集设计器自身的使用计数与引用列表并存 → Acceptance notes(打磨,未立卡)。Skills-docs: 不适用(Metadata-protocol: none)。
  • 落地条款:非受管、≤3000 行 ⇒ 席位经中继落地(维护者授权见本会话)。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 02:50
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 02:50
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 6f095b9 Nov 5, 2026
59 of 60 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-30064-studio-dataset-customize branch November 5, 2026 03:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: customize an installed package's dataset, showing what else it changes before the customer publishes

1 participant