Skip to content

fix(app-shell): a member takes back their own comment reaction through the unreact door - #26384

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-6tmyk6
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-6tmyk6

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #26346

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:记录动态里点自己的回应撤回时,改走服务端新开的「撤回自己回应」专用接口 DELETE /api/v1/comments/reactions/:id,不再走通用数据删除接口;撤回按钮的权限判断不再要求 delete 授权。
  • 为什么改:普通成员(只有全员默认授权、没有职位)能加回应却永远撤不回:通用删除要对象级 delete 授权,而全员授权集按 ADR-0090 D5 不能带它。服务端那一半已在 PR fix(plugin-audit): a member removes their own comment reaction through an own-row unreact door #26345 落地。
  • 风险与回滚:风险低,只动回应芯片的撤回一条写路径和地图里一行;别人的回应仍然不会出现撤回(点了是加自己的)。回滚即 revert 本 PR,旧路径回来,普通成员重新撤不回。
  • 先看:① ui/packages/app-shell/src/views/RecordDetailView.tsx 的 handleToggleReaction 里 write 的撤回分支;② ui/packages/core/src/utils/affordanceGrants.ts 的 feedReactionRemove 行;③ RecordDetailView.feedGrant-12082.test.tsx 新增的 describe 块。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx,预期全绿,其中 "a member with no grant beyond create on sys_comment_reaction adds a reaction and takes it back" 断言请求打到 /api/v1/comments/reactions/new-row、方法 DELETE。
  • 需要你做的:无,席位落地。

What changes

  • The take-back's door. On the records store, a click on the caller's own reaction chip now removes the row through the unreact door, DELETE /api/v1/comments/reactions/:id (mounted by @objectstack/plugin-audit, server half in fix(plugin-audit): a member removes their own comment reaction through an own-row unreact door #26345), called through the page's existing authenticated fetch and VITE_SERVER_URL base, the way the record page already reaches non-data routes. No body and no query parameter are sent. A non-2xx answer is a refused write: the existing settle step puts the reaction back and raises the one "Your reaction was not saved" toast. The generic DELETE /api/v1/data/sys_comment_reaction/:id is no longer called from the feed. No @objectstack/client method is added.
  • The affordance row. feedReactionRemove in AFFORDANCE_GRANTS changes value from { crud: 'delete', grant: 'delete' } to { crud: null, grant: 'create' }. crud: null because the write no longer goes through the object's data door (the same meaning the attachment rows carry); grant: 'create' because the take-back is offered to whoever may react. The row's TYPE keeps its shape (AffordanceGrantRow, same two keys) and ConsoleAffordance keeps the same member set; no export is added or removed.
  • Mine. Which chip offers a take-back is unchanged: the click handler already takes back only when the caller's id is among the emoji's reactors, and removes only the row id the read or the create recorded for the caller (groupReactionRows keys on the row's server-stamped user_id). A click on another member's chip adds the caller's own reaction.
  • Docs. ui/content/docs/plugins/plugin-detail.mdx no longer says a take-back needs the delete grant.
  • sdui record. None. The per-package records were retired by ci(sdui): retire the per-package SDUI manifest records; CI's regeneration holds freshness (#26137 PR B) #26367 while this PR was open. The main merge took the modify/delete with git rm, and CI's Build & E2E UI regenerates the manifest (sdui.manifest.json unchanged: 106 components).

Tests

New regression cases (in RecordDetailView.feedGrant-12082.test.tsx, describe "a take-back goes through the unreact door, and only on the caller's own reaction"):

  • a member with only the create grant on sys_comment_reaction adds 🎉 and clicks it again: one call to /api/v1/comments/reactions/new-row with { method: 'DELETE', credentials: 'include' }, no data-door delete, and the chip goes away;
  • another member's 👍 chip: the click creates the caller's own row, and the unreact door is never called;
  • the door answers 403: the reaction is put back (count 1) and toast.error is raised once.

Forced test edits:

  1. ui/packages/app-shell/src/views/RecordDetailView.feedGrant-12082.test.tsx — the authenticated-fetch mock now returns a hoisted mock (it returned a bare vi.fn()); "a take-back asks the delete grant … writes nothing" becomes "a take-back reads no delete grant … removes it through the unreact door"; the full-grant CONTROL and the registered-schema case assert the door's URL instead of dataSource.delete(sys_comment_reaction, r1).
  2. ui/packages/plugin-form/src/affordanceGrantMap-12082.test.tsx — EXPECTED_GRANT.feedReactionRemove is 'create'; the write-census entry RecordDetailView > handleToggleReaction > write :: delete is deleted, since that site no longer calls a DataSource write (the census states it does not see a write through its own route, the same as the attachment upload).
  3. ui/packages/app-shell/src/views/RecordDetailView.reactionRecords-12078.test.tsx — not named in the dispatch; forced because its fake server asserted the take-back as delete(sys_comment_reaction, id). The fake server now also models the unreact door per member (removes the member's own row, 403 on anyone else's), and the two take-back cases assert the removed ids at the door and that delete is not called.

Acceptance notes

  • The door authorizes on the row's created_by; the chip decides "mine" on the row's user_id. Both are stamped server-side from the reacting session at create (the create hook stamps user_id, the engine stamps created_by), so they name the same member for every row made through the data door. Keying the chip on created_by was not done here (it would move the read's grouping too); noted for the seat.
  • The feedReactionRemove row now asks the same grant as feedReactionAdd, and the reaction control is only handed over when feedReactionAdd allows it, so on the records store the take-back is in practice decided by ownership alone, as the card asks.

Verification

Tests and ablation

Commands and readings are in the os-dev-report on #26346, taken at this PR's head.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADHkmPysaCwFvT4Pqb88z8

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · seat domain:ui#1 · 2026-10-27T02:09Z

Head abd84b67 (read from the PR object). It merges main e931621a, after #26367 retired the per-package sdui records.

  • Shape. Draft on main, Fixes #26346. 7 files, +190/-44.
    • Taking back the caller's own reaction now calls the server's own-row door, DELETE /api/v1/comments/reactions/:id (fix(plugin-audit): a member removes their own comment reaction through an own-row unreact door #26345), through the page's authFetch. It no longer uses the generic sys_comment_reaction data delete, which no every-member set may grant.
    • feedReactionRemove in affordanceGrants.ts changes value only: { crud: 'delete', grant: 'delete' } → { crud: null, grant: 'create' }. A member who may react may take back their own reaction. "Mine" is decided client-side from the row's id and user_id, so another member's chip still offers no remove.
    • Three forced test edits, named in the body: feedGrant-12082, affordanceGrantMap-12082, and reactionRecords-12078, whose fake server asserted the generic delete and now models the door per member.
    • One sentence in plugin-detail.mdx and a root changeset. Clause-②: no, Metadata-protocol: none.
  • Clause-② check (seat ask). AFFORDANCE_GRANTS is as const, so the published d.ts literal of this one row moves with its value. In this repo, every reader assigns the row to AffordanceGrantRow or indexes by key. ConsoleAffordance and FieldAffordance member sets are unchanged, and no export is added. A value change on an existing key is neither a wider accept set nor a larger surface, so no stands.
  • Checks pinned to the head. All check runs completed: 0 failing, 0 pending. The seat's landing script checked this before posting.
  • Governed surface. check-governed-merges.mjs --pr 26384: not governed.
  • Evidence (dev, on abd84b67).
    • core / app-shell / plugin-form type-check exit 0.
    • Reaction files, the feedGrant pin, the map pin and core utils: 112 files / 2011 tests.
    • 35/35 gates, 0 NOT-MEASURED.
    • Security ablation: the base source turns 8 of the new cases red, restored by hash.
  • Serial constraints. RecordDetailView.tsx is shared with console: a non-participant reader is offered inline edit on a record whose pending approval locks it (lockRecord: true), and the save answers 409 RECORD_LOCKED #25934 (seat 1, building), which lands after this. No other open PR shares a file. The ⛔ paths are untouched.

Landing: ready + auto-merge through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 02:10
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 02:10
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 48a9d9d Oct 27, 2026
61 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-6tmyk6 branch October 27, 2026 02: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/m tooling

Projects

None yet

1 participant