Skip to content

feat(studio): a read-only base and its customization pair as one surface - #28747

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-11viyv
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-11viyv

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #28572

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 把只读基础包和它的定制包 .custom 合成一个界面。四个栏位(数据、自动化、权限、界面)在同一个列表里同时列出基础包的项和定制包新增的项,并标出「已定制」或「新增」。每个栏位多了一个「仅看定制」开关。包切换器不再单独列出定制包,对子地址会跳到基础包。在只读基础包上点「新建」,会经由 ensure 接口建到定制包里。
  • 为什么改:ADR-0146 D3 要求「一个界面」。之前定制包在切换器里像另一个包,作者要来回切换,也看不出哪些项被定制过。
  • 风险与回滚:只改了 app-shell 的 studio-design 目录和 studio 文案(en/zh),没有改协议、服务端或 spec。可写包,以及没有定制包的只读包,读取和显示都和原来一样(有对照测试)。回滚就是 revert 这个 PR。
  • 先看:StudioRailCustomizations.tsx(共用的合并、标记、筛选和深链兜底逻辑),然后看 StudioDesignSurface.tsx 里的 StudioDesignSurface 外壳(对子地址跳转,以及 metadata 区不跳转)和 InterfacesPillar 的 doCreateSurface(新建进对子)。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/app-shell/src/views/studio-design/StudioDesignSurface.oneSurface-28572.test.tsx,10 条全过。
  • 需要你做的:不需要拍板。第三方包贡献的项(扩展字段、基础对象上的流程、D7 导航条目)没有画出来,因为没有任何已服务的读取说明是哪个包贡献的。这部分的服务端工作见 the card runtime: a served read naming which package contributes to an installed base — extension fields, flows on its objects, navigation entries — so Studio can draw contributed items greyed with their source (ADR-0029 D7, ADR-0146 D3) #28722。

What changes

Studio now shows a read-only base and its paired customization package (<base>.custom) as one surface (ADR-0146 D3, "On top, one surface"). Everything is in ui/packages/app-shell/src/views/studio-design/** and the studio i18n namespace.

  • One list per rail. The Data, Automations, Access and Interfaces rails list the base's items, then each item of the pair whose name the base does not hold, stamped addedIn: <pair> (withPairItems). An override the pair serves under the base's name stays the base's row, as ADR-0146 D8 item 2 serves it. The Interfaces rail does this for pages, dashboards, reports and views (its off-nav sections).
  • Marks. Rows are marked from the base's N-15 read (GET /api/v1/packages/:id/customizations) through the one rule customizationMarks.ts already holds. An override the read lists is "customized". Any other row of the pair is "added". Every other row has no mark. Nothing is computed from the documents. A failed read marks nothing and shows the server's sentence under the rail's search. The read refreshes on Publish.
  • Only customizations. Each rail gets an "Only customizations (n)" toggle under its search (StudioRail's new filters slot), shown once some row carries a mark. On Interfaces it lists every marked item in place of the nav tree, including ones a nav entry opens.
  • Package switcher. A pair whose base is listed has no row of its own. The base's row names it ("with <pair>") (foldCustomizationPairs).
  • The pair's URLs. /studio/<pair>/<pillar> lands on /studio/<base>/<pillar>, with its query and router state kept. The pairing comes from the spec's rule (customizationBasePackageId), so the redirect never waits on the package list. A ?surface= link to an item the pair adds opens it once the pair is known (useDeepLinkFallback). The rail's first read happens before the package list answers, so it opens its default first. It then switches to the linked item if the author has not moved off that default.
  • Items the pair adds open writable on the base's surface. They are read and saved at the pair their row was listed in (Surface.addedIn and its siblings in the Data, Automations and Access pillars). They are saved as they are, with no override declaration. They show no Customize and no read-only link, and the item header names the pair (StudioPairItemChip).
  • New on a read-only base goes through the ensure door (StudioBaseCustomization.ensurePackage) in all four pillars. It then saves into the package the door answered, with the door's namespace where a name is prefixed (objects). After the create, each rail re-reads, so the new row is listed as added. On Interfaces, the "+" offers only page, dashboard and report there. The created item links no nav entry, because the base's app takes no edit. The dialog says where it lands. A walled admin is offered no New on a read-only base.
  • Unchanged. A writable package, and a read-only base with no pair, read and draw as before. Customized items keep the card Studio: customize a read-only package's view, dashboard or report into <base>.custom — a declared override, its badge, and "Restore package original" (replaces #28147) #28562 Customize path (override declaration, badge, Restore).
  • Strings. Five keys go in the studio namespace (en, zh). That namespace is typed Record<string, string> and sits outside the locales packs, so Clause-②: no holds as claimed.

Named decisions and deviations

  1. Contributed items are not drawn. No served read names the package that contributes extension fields, flows on the base's objects, or ADR-0029 D7 nav entries. The registry's merged object carries no per-field source, and flow and nav reads name none either. Nothing third-party is greyed or listed. The test nothing a third package contributes is drawn pins that no rail asks another package's list and no row names one. The server side is the card runtime: a served read naming which package contributes to an installed base — extension fields, flows on its objects, navigation entries — so Studio can draw contributed items greyed with their source (ADR-0029 D7, ADR-0146 D3) #28722.
  2. Write target. A create names only the pair the ensure door answered. An edit of an added item names the pair its row was listed in (addedIn, taken from the read that listed it). ⛔ No write names a .custom id spelled in the browser. The spec's pairing rule is used for READS only: the redirect, and matching a "New similar" arrival to the rail (pairIdOf).
  3. /studio/<pair>/metadata is not redirected. The "All metadata" area lists the pair's items under ?package=<pair> and re-roots there, so redirecting it would loop. The test the pair's "All metadata" URL stays on the pair pins this. Ablating the exemption reddens it.
  4. Extended base objects carry no rail mark. A field the pair adds to a base object makes the object neither an override nor an added item under the one mark rule. The extension count is on the All metadata directory (the card Studio: All metadata lists show a base's .custom items marked "customized" or "added", with an "only customizations" filter and per-type counts #28568). The Data test pins the object as unmarked.
  5. An object create on a read-only base asks the door before the name check. The name's namespace prefix is the door's answer, so a refused or taken name can still leave a newly created, empty pair behind.
  6. Known gap, not widened. The report dialog's "New dataset" on a read-only base saves into the open package, which is the base while creating, so the server refuses it as before. This PR does not route datasets.

Tests

  • New: StudioDesignSurface.oneSurface-28572.test.tsx, 10 tests. It mounts the real StudioDesignSurface and metadata client over a fetch stub. Covered:
    • the customized and added marks, and no mark on the base's own rows;
    • the filter showing exactly those two rows;
    • an added item saving at the pair with no override, and the door not asked;
    • New on the read-only base asking the door (200), saving into the pair, with no app save;
    • the Data rail's added object, and its header chip;
    • the switcher with no pair row and the base naming the pair;
    • the pair URL landing on the base and opening the linked added item;
    • the metadata area not redirected;
    • no third-party read or row;
    • CONTROL: a base with no pair reads no pair list, and draws no mark and no filter.
  • Updated: InterfacesPillar.customize-28562.test.tsx (its host passes the new ensurePackage and marks) and StudioCustomizeControls.customize-28562.test.tsx (the door answer now carries namespace).
  • Ablations, each run through scripts/ablation-replace.mjs against the new file, with the file restored to HEAD each time. Each went red:
    • the metadata exemption;
    • the pair redirect;
    • the addedIn stamp;
    • the switcher fold;
    • New's pair target;
    • the added item's save target;
    • the filter;
    • the deep-link upgrade;
    • the pair read.

Acceptance notes

Verification

  • pnpm --filter @object-ui/app-shell --filter @object-ui/i18n run type-check (source and tsconfig.test.json): exit 0. ESLint on the touched files: 0 errors. check:i18n-keys, check:i18n-drift and check:i18n-dead-keys (from ui/): exit 0.
  • The studio-design suite (288 files) plus studio-locale.i18n and zh-base-term.i18n-13131, from ui/ under os-verify-lock: all passed. Because the deep-link fallback landed after the first three chunks ran, the 14 files there that touch ?surface= links were run again at the final code: 14/14.
  • packages/i18n/ (94 files): passed.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 34 commands, run and reconciled with --ran (results in the card report).
  • Merged origin/main twice (7 commits, then 5, no conflict). Neither range touches studio-design/** or the studio namespace, so there is no parallel same fix. The gate sweep and the suites ran at 65e3b1b, and check-sdui-manifest was re-run at the head.

https://claude.ai/code/session_01QsR2jV3gDN72FtkqvGmfBd

🤖 Generated with Claude Code

…urface

Each Studio pillar rail lists the pair's added items beside the base's,
marks customized and added rows from the served customization read, and
offers an only-customizations filter. The package switcher folds the pair
under its base, a pair's pillar URL lands on the base, and New on a
read-only base lands in the pair through the ensure door.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jV3gDN72FtkqvGmfBd
Pins the marks, the only-customizations filter, the switcher fold, the
pair URL redirect and its metadata-area exemption, New into the pair, the
added item's save at the pair, and that nothing third-party is drawn. A
deep link to an item the pair adds opens once the pair is known.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jV3gDN72FtkqvGmfBd
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tooling labels Nov 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

席位核对 · ACCEPT · 2026-11-01T17:07Z

Head-sha: deb602a61eaa3d685ab05044dab3e7bd59e42c77


Generated by Claude Code

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 41330449085 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Type Check UI — 失败步骤: Run type-check over the ui packages(日志不可读,点进 job 看)

  • Test UI (3/16) — 失败步骤: Run the ui tests (shard 3/16)

    × on a read-only base, the editor is handed the surface's customization: the base, its version and the door 79ms
    FAIL   dom  packages/app-shell/src/views/studio-design/StudioDesignSurface.metadataArea-28561.test.tsx > the area's form editor in the builder (objectstack#28569) > on a read-only base, the editor is 
      ↳ 失败原因: AssertionError: expected { base: 'com.vendor.hotcrm', …(5) } to deeply equal { base: 'com.vendor.hotcrm', …(4) }
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • packages/app-shell/src/views/studio-design/StudioDesignSurface.metadataArea-28561.test.tsx — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 1 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 1, 2026
Brings in the All metadata area's form editor. Its two tests construct and
compare the surface's customization, which now also carries ensurePackage and
the base's customization read.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jV3gDN72FtkqvGmfBd
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

席位核对 · ACCEPT (re-check after the queue failure) · 2026-11-01T18:11Z

Head-sha: ff0be402d137d508e1677ab8abd8352b029d29c7

This replaces the ACCEPT at deb602a6, which the merge queue dropped on a semantic conflict with #28743 (#28569's form editor): Type Check UI and Test UI 3/16 went red.

  • What moved: one commit, ff0be402, a merge of main at 7a48bf73. Read with git diff-tree --cc, only two files differ from both parents, and both are tests:
    • StudioBundleItemEditor.customize-28569.test.tsx: the test host's customization now carries ensurePackage and marks: { status: 'off' }, the two members this PR adds to StudioBaseCustomization.
    • StudioDesignSurface.metadataArea-28561.test.tsx: the expected customization adds marks: expect.any(Object).
    • StudioDesignSurface.tsx and the studio packs merged cleanly. No product line is new to both parents.
  • The PR against main now: 14 files, +1386 / −155 (1541 lines ≤ 3000). check-governed-merges --pr 28747: 0 of 14 paths hit the register. These are the 12 files of the first ACCEPT plus the two test files above. The declarations, the wanted items, the seat's conditions and the accepted deviations are unchanged from the first ACCEPT.
  • Gates (check-runs on this head): 44 success, 11 skipped, 0 failed or pending. All ten required contexts are included. The dev also ran the full views/studio-design suite locally at this head: 288 files and 2111 tests passed.
  • Landing: ordinary queue landing. pr_ready (a no-op, already ready) then automerge_enable. Fixes #28572 closes the card on merge.

Generated by Claude Code

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/xl tooling

Projects

None yet

1 participant