Skip to content

feat(app-shell,plugin-list): a personal view's own conditions are Filter panel rows, saved back by 保存 - #30242

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-30073-personal-view-conditions
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-30073-personal-view-conditions

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #30073

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:打开「我的视图」时,它自己的筛选条件以普通行出现在「筛选」面板里;改动条件后出现「已个性化」,标记里提供「保存」,把面板写回这个视图;「另存为我的视图」之后,原来那个视图的临时条件和「已个性化」一并清掉。
  • 为什么改:按裁决 A(「其他全部同意」):销售要能直接调整自己视图的条件,不必重建再删除;另存后原列表不再少行、不再像是数据丢了。
  • 风险与回滚:只动 Console 前端;存储形状不变、不加新键。面板容纳不了的条件(「或」分组夹在其他条件旁)或读不到的字段,仍按原样在列表底下生效、不进面板。回滚即还原本 PR。
  • 先看:ui/packages/app-shell/src/views/ObjectView.tsx(保存与另存后的清理)、ui/packages/app-shell/src/views/personalViews.ts(条件变行、保存体)、ui/packages/plugin-list/src/ListView.tsx(「已个性化」的筛选判断)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/ObjectView.personalViews-29635.test.tsx,预期 20 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

Ruling A on #30073 (comment 6631079402), the maintainer's words 「其他全部同意」: a personal view's conditions appear as ordinary rows in the filter panel, editing them offers 保存 to that view, saving a view also clears the source view's temporary conditions, and conditions that cannot be folded keep today's refusal. The stored shape does not change and no key is added.

  • Opening a personal view seeds its conditions as Filter panel rows (personalViewPanelFilters in personalViews.ts, reading the stored list through @object-ui/core's builderRulesOfSpecFilter, the read direction of the fold). While the panel holds them they are the list's filter: the renderer no longer applies the stored filter under the list as well, so each condition is applied once. A stored list the panel cannot hold (an any-of group beside other conditions), or one naming a field the object does not declare or the reader cannot read, keeps today's behaviour: applied under the list, not drawn as rows. The field gate is the one the list state is read back through, so a row it would drop on reload never makes the view select more records than it declares.
  • The 「Personalized」 judgement for filters (ListView): a host that shows the view's own conditions as rows passes them as viewDefaults.filters; the panel's finished conditions are then compared with them (field, operator, value, and the logic once two rows make it matter), so the view's own rows are not a personalization and an edit, a removal or "Clear all" is. Without the key, behaviour is as before.
  • 保存 in the 「Personalized」 marker, on a personal view, for a reader allowed to update sys_personal_view: it builds the body with buildPersonalViewConfig (the one Save as my view uses, so the stored ListView shape is the same), replaces the row's config through the data API's update, then clears the reader's list state for that view and the list opens again on the saved view. Nothing is written before the click. A panel the fold refuses (nested_group) shows the same refusal Save as my view gives and writes nothing.
  • Save as my view clears the source view: after the row is created, the source view's stored list state and cached search term are cleared and the address being left loses its uf_* params (clearSourceListState), so neither Back nor the next open of the source brings the conditions back or reads 已个性化.
  • Both saves read the panel as the list reports it (readPanelFilters), not the address's copy: an emptied panel leaves no uf__filter param, exactly as an untouched one, so the address alone could not tell "removed every condition" from "unchanged".

Landing point: ui/packages/app-shell/src/views/** and the filter-state region of ui/packages/plugin-list/src/ListView.tsx (the viewDefaults prop type and the personalization judgement); the hidden-fields panel is untouched.

Tests

  • personalViews.test.ts: personalViewPanelFilters (AND list, the fold's OR group, and the three cases that stay under the list); buildPersonalViewConfig with panel-held conditions (untouched keeps the stored list, edited is the whole filter and parses under ListViewSchema, emptied stores none, nested refuses).
  • ObjectView.personalViews-29635.test.tsx, new top-level describe in the shared harness: (1) opening pv_mine shows Filter, 1 active, the Paris row in the panel, Paris once in the list query, no marker; (2) an edited panel shows the marker, 保存 calls update('sys_personal_view', 'pv_mine', { config }) with filter: [{ field: 'city', operator: 'equals', value: 'Rome' }], the body parses under ListViewSchema, and the list reopens unpersonalized on Rome; no 保存 without the update grant; (3) Save as my view from a filtered v1 stores the conditions in the new view, clears v1's stored list state, and a fresh open of v1 has no condition and no marker; the nested-group panel keeps the refusal and writes nothing; a stored list the panel cannot hold stays applied under the list.
  • ListView.personalization-28950.test.tsx: viewDefaults.filters equal rows report nothing, edited or emptied rows report filters.

Acceptance notes

  • personalViewPanelFilters builds builder rows the way the Studio inspector's private groupOfStoredRules (metadata-admin/widgets.tsx) does; that file is outside this card's surface, so the two are not merged here.
  • A link to a personal view that carries uf__filter now opens with the link's rows as the whole panel (the panel is the filter), rather than those rows laid over the view's stored conditions. Personal views are the reader's own and the address this page writes already carries the whole panel.
  • 保存 and Save as my view clear the reader's whole stored entry for the view (sort, filters, column layout, density, hidden fields): every part that entry holds is in the saved body, except column widths, which no saved view body carries today.

Verification

All readings on head 3ee4384a5 (after merging origin/main), objectstack-ai/objectstack.

Tests (isolated, as CI shards run them)
  • pnpm --dir ui exec vitest run --isolate --maxWorkers=2 packages/plugin-list/: Test Files 189 passed (189), Tests 1799 passed (1799); lock VERDICT command-exit 0.
  • app-shell, narrowed and declared: the 128 app-shell test files that import a touched module (ObjectView, objectViewListRenderer, objectViewListUrlState, personalViews, ListPersonalizedMarker), --isolate --maxWorkers=2: Test Files 128 passed (128), Tests 1125 passed (1125); lock VERDICT command-exit 0. The whole package (2177 files) does not fit a local run at two workers; Test UI in CI runs it. The narrowing is by import and is blind to a test that reaches these modules through another import.
  • pnpm --dir ui exec vitest run scripts/ (after the seven @objectstack builds): Test Files 170 passed, 2 skipped (172); Tests 4985 passed, 2 skipped.
  • pnpm exec turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/plugin-list (each runs tsc --noEmit and tsc -p tsconfig.test.json): Tasks 69 successful, 69 total.
Reverse verification (each mutation applied by `scripts/ablation-replace.mjs`, which proved it landed and restored it; tree clean after each)
  • Base filter applied again for panel-held conditions (const base = false ? undefined : (): "applied once" goes red, expected [ 'Paris', 'Paris' ] to have a length of 1 but got 2.
  • clearSourceListState(activeView.id) deleted: "Save as my view leaves the view it came from ..." goes red, the source's stored filters still present.
  • viewDefaults.filters ignored in ListView: "applied once ... not personalized" goes red, the marker is drawn.
Gates

node scripts/pm/dispatch-gates.mjs --commands derived 35 commands; all 35 run, each exit 0 (check:type-check-debt first returned exit 3 PREREQUISITE NOT MET on a lock queue timeout, then re-run under the lock: re-measure: OK, exit 0). --ran reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Also pnpm check:changeset-fixed (roster under .changeset) exit 0 and pnpm check:new-line-citations 0 new citations.


Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

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

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

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

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

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

    × a read-only package: overlay types can be customized, never 可写; the create-only and closed types are read-only 75ms
    × a read-only package: the view list header reads 可定制, the page list header 只读 8ms
    × typeStateWord: the mapping in every context 6ms
    FAIL   dom  packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx > All metadata tiles: one state word, in the package context (objectstack#29705) > a read-only pack
      ↳ 失败原因: AssertionError: expected '可定制' to be '只读' // Object.is equality
    FAIL   dom  packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx > the list header draws the tile word (objectstack#29705) > a read-only package: the view list head
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx > the state vocabulary census (objectstack#29705) > typeStateWord: the mapping in every context
      ↳ 失败原因: AssertionError: expected [ Array(4) ] to deeply equal [ 'customizable', 'readOnly', …(2) ]
    

↳ 失败原因 是判读的关键:超时(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,按失败测试文件聚合):

历史信号:

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

分诊清单:

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

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

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

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

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

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

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

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

    × no module in this repository imports the deep module except the barrel 389ms
    FAIL   unit  packages/types/src/__tests__/strict-authoring-face-8345.test.ts > the barrel is the sole entry into the module cycle > no module in this repository imports the deep module except the barr
      ↳ 失败原因: Error: ENOENT: no such file or directory, open '/home/runner/work/objectstack/objectstack/ui/packages/create-plugin/tsup.config.bundled_xhmfdr22t5.mjs'
    

↳ 失败原因 是判读的关键:超时(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/types/src/__tests__/strict-authoring-face-8345.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-justin · session_01QSeKYpFBMr917PmVkG9up7

Queue red, re-queued once · 2026-11-05T04:05Z

Two queue builds went red. The last one dequeued this PR (CI_FAILURE, 04:04).

  1. Run 41962404343 · DirectoryPage.stateVocabulary-29705.test.tsx (AssertionError on 可定制/只读). That test and PageShell.tsx belong to fix(metadata-protocol,app-shell): serve a pair's mapping override, let a pair remove any override it holds, name a working remedy for an excluded type (#30062) #30226, which was ahead in the same speculative stack and has since been removed from the queue. This PR touches no metadata-admin file. Anchor: Queue-flake anchor: packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx #30253. Not this PR's.
  2. Run 41962914067 · ui/packages/types/src/__tests__/strict-authoring-face-8345.test.ts: ENOENT … ui/packages/create-plugin/tsup.config.bundled_xhmfdr22t5.mjs. New signature. Initial read: a race inside the test. Its repo walk guards readdirSync but not the per-file readFileSync, and tsup deletes a transient bundle between the list and the read. Filed as strict-authoring-face-8345 test walks the repo with an unguarded readFileSync and races tsup temp bundles (queue flake) #30256 (the check-error-status-conformance.mjs walk() races tsup's transient packages/spec/tsup.config.bundled_*.mjs: readdir-then-stat throws ENOENT and turns the required Test Core (1/6) red with 597 / 597 tests passed #20225 / [finding] lint-startup-registry-verdict corpus test walks packages/ with a bare readdir-then-stat inside @objectstack/lint#test, so tsup's transient bundled config fails it with ENOENT (measured 4 of 8) #20269 family).

Re-queue once, on the three facts:

  • The failing file's import closure does not meet this diff. This PR touches only ui/packages/app-shell and ui/packages/plugin-list. The failing read was a create-plugin temp file.
  • The queue base is green on the same shard. main check-runs read Test UI (4/16) success.
  • The first error is not an assertion of product behaviour. It is an ENOENT on a file that vanished mid-walk, a timing-sensitive premise in the timeout class. The test's invariant assertion never ran red.

If the same signature fires again, the seat stops re-queueing and hands it on via #30256.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit eb6199b Nov 5, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-30073-personal-view-conditions branch November 5, 2026 04:29
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] A personal view's own conditions are invisible and cannot be edited: how should a reader change a saved view?

1 participant