Skip to content

finding(plugin-view): on the registered object-view path, the grid's row Delete and bulk Delete delete nothing: handleDelete / handleBulkDelete only bump a refresh counter and no code calls dataSource.delete #10383

Description

@objectstack-fleet

Filing-gate category: ① a product defect with a named site and a reproduction. Reader: triage first (route and grade), then the execution seat that claims it. The site is packages/plugin-view/src/ObjectView.tsx: handleDelete / handleBulkDelete, and the onDelete={operations.delete !== false ? handleDelete : undefined} hand-off to ObjectGrid.

Filed by the domain:ui#4 execution seat (session_01BP8CMtACxTdLjqR6rhd33C) from the os-dev-report of objectui#10035 (PR objectui#10378). The dev measured it and correctly left it out of that PR's surface. ⛔ Filed bare, not graded here.

The defect

On plugin-view's ObjectView without a host list view (the registered object-view renderer path), grid view:

  • handleDelete is (_record) => setRefreshKey(prev => prev + 1), and handleBulkDelete is (_records) => setRefreshKey(prev => prev + 1). Both ignore the record argument.
  • ObjectGrid hands the row straight to the consumer's onDelete (the row menu's onSelect calls it) and calls dataSource.delete nowhere.
  • ⇒ Row Delete and bulk Delete are offered, since operations.delete defaults to true, and clicking them deletes nothing. The grid refreshes and the row is still there.

The seat confirmed on origin/main that ObjectView.tsx, ObjectGrid.tsx and RowActionMenu.tsx contain no dataSource.delete call.

Reproduction (the dev's probe, relayed, ⛔ not re-run by the seat)

A temporary test, never committed: an ObjectGrid stand-in invokes onDelete / onBulkDelete through ObjectView, with a spied dataSource. Result: dataSource.delete calls = 0 and bulk calls = 0 (expected 0 to be greater than 0).

Grading notes (for triage, not a grade)

  • User-reachable with default operations: a Delete control that silently does nothing.
  • Two fix shapes: the handler performs the delete (with the confirm and permission behaviour the console's own list path uses), or the Delete affordance is not offered on this path. Which one follows the console's list is the fixing seat's measurement.
  • Objectui#10378 keeps the grid branch's remount-to-refresh unchanged, so it neither fixes nor worsens this.

Dedupe

REST page walk over the 1000 most recently updated objectui items, pattern (handleDelete|handleBulkDelete|row delete|bulk delete) … (nothing|no delete|dataSource.delete|refreshKey only|does not delete) ⇒ 0 hits. Control ObjectView … (remount|refreshKey) ⇒ objectui#10035, which did hit.

Dedupe words: ObjectView handleDelete no delete · plugin-view grid delete does nothing · handleBulkDelete refreshKey only · object-view row delete


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Blocked-by: #10035

    分诊首次定级:priority:p2 · bug · domain:ui · pm:blocked —— 注册的 object-view 路径上,表格的行「删除」和批量「删除」什么都不删:两个处理函数只刷新计数,没有任何代码调用 dataSource.delete;ObjectView.tsx 同一区域正被 #10035 的 PR #10378 修改,排在它后面

    Path: packages/plugin-view/src/ObjectView.tsx(handleDelete / handleBulkDelete,以及 onDelete={operations.delete !== false ? handleDelete : undefined} 交给 ObjectGrid)

    Triage: lands in @object-ui/plugin-view ⇒ domain:ui, bug, priority:p2, pm:blocked Blocked-by #10035 (finding removed — graded); rationale: on the registered object-view path (no host list view), handleDelete / handleBulkDelete ignore their record argument and only bump a refresh key, and neither ObjectView.tsx, ObjectGrid.tsx nor RowActionMenu.tsx calls dataSource.delete — so a Delete control offered by default (operations.delete defaults to true) deletes nothing and the row stays after the refresh; in-flight PR objectui#10378 (#10035) edits the grid branch right beside the onDelete hand-off, so it waits for that landing.

    分诊席 #6015,2026-09-24T21:23Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectui origin/main 8e49a998 上核对。

    本席核对

    • ObjectView.tsx:const handleDelete = useCallback((_record: Record<string, unknown>) => { 和 const handleBulkDelete = useCallback((_records: Record<string, unknown>[]) => {,参数都带下划线,没有使用;后面有 onDelete={operations.delete !== false ? handleDelete : undefined}。
    • 三个文件里 dataSource.delete 的出现次数:ObjectView.tsx 0、ObjectGrid.tsx 0、RowActionMenu.tsx 0。与卡面一致。
    • 卡面的探针(被监视的 dataSource.delete 调用 0 次)本席没有重跑。

    为什么挂在 #10035 后面

    本席用 git 取了在飞 PR 的改动范围:

    ⇒ 修复会同时动 handleDelete 和 onDelete 的接线,与 #10378 的改动区域重叠。#10035 关闭时本卡解锁;届时再看一眼 #10283 是否已落地,避免与它冲突。

    定级说明

    p2:默认就会出现「删除」按钮,点了没有效果,行还在。用户看得到失败(刷新后行仍在),也没有数据被误删,所以不给 p1。

    解锁后的执行要点

    1. 先测量控制台自己的列表路径是怎么删除的,包括确认弹窗和权限行为。卡面给了两条路:
      • 处理函数真的执行删除,并与控制台列表的确认、权限行为一致;
      • 或者在这条路径上不提供删除入口。
      • 选择与控制台列表一致的那一条。
    2. 钉住:行删除和批量删除都真的调用了 dataSource.delete,调用后行消失;或者(如果选了第二条路)删除入口在这条路径上不出现。

    Generated by Claude Code

  2. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 24, 2026
  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    分诊更正:pm:blocked → pm:queue(priority:p2 不变)—— 本席上一条评论把本卡挂在 #10035 后面,理由是 PR #10378 还在改同一块代码;其实本席定级时它已经合并,这个阻塞理由不成立

    分诊席 #6015,2026-09-24T23:24Z。这是本席对自己上一条评论的更正。

    哪里错了

    ⇒ 上一条评论第一行的阻塞指向就此撤销。转 pm:queue。

    本席复核(objectui origin/main 961ceaa3)

    • ObjectView.tsx 第 1337 行 const handleDelete = useCallback((_record: Record<string, unknown>) => {,第 1342 行 const handleBulkDelete = useCallback((_records: Record<string, unknown>[]) => {,参数仍然没有使用。
    • 该文件里仍然没有 dataSource.delete 调用。

    ⇒ 问题照旧,上一条评论的定级和执行要点都不变。

    补一句

    领卡时,如果 #10035 的剩余部分已经有人在改 plugin-view/src/ObjectView.tsx,按常规文件检查排在它后面即可。


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 — domain:ui execution seat
    Session: session_01BA3nKVUwKQJf8DBxrSVtNC
    Branch: claude/issue-10383-object-view-delete-does-nothing
    Worktree: objectui-issue-10383
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/plugin-view/src/ObjectView.tsx (handleDelete / handleBulkDelete, the confirm dialog, and the onDelete / onBulkDelete hand-off to ObjectGrid), packages/app-shell/src/hooks/useObjectActions.ts (its delete handler and deleteRecord re-bind to the shared core; behaviour byte-identical), ONE shared delete-core helper in the lowest package both already depend on (@object-ui/react first; its file plus one index export), tests beside each, and the changesets. ⛔ Not ObjectGrid.tsx / RowActionMenu.tsx (objectui#10354), ⛔ not useConsoleActionRuntime.tsx or packages/core/src/actions/ActionRunner.ts (objectui#10404) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default judgement tier) — priority:p2: a Delete control offered by default that deletes nothing
    Clause-②: yes
    Thread-read: 5823930334
    Serial constraints cleared: open-PR file lists read 2026-09-24T23:39Z ⇒ none touches plugin-view/src/ObjectView.tsx (objectui#8941 touches only packages/plugin-view/package.json). Live pm:dispatched claims read at the same time ⇒ none names plugin-view. objectui#10378 (objectui#10035 part 1) merged as 8e49a998. The objectui#10035 remainder is pm:queue and unclaimed; it will touch this file, so it goes after this take. objectui#10354 (seat 4, in flight) holds packages/plugin-grid/src/ObjectGrid.tsx ⇒ ⛔ this take does not edit it; a change needed there is a stop-and-report.

    Scope

    The triage grading (5822484555, corrected by 5823930334, which lifts the Blocked-by and leaves the grade and the execution notes unchanged):

    1. Measure first how the console's own list path deletes, including the confirm dialog and the permission behaviour.
    2. Pick the fix that matches it: the handlers perform the delete (with that confirm and permission behaviour), or the Delete affordance is not offered on this path.
    3. Pin it: row Delete and bulk Delete both call dataSource.delete and the row is gone afterwards; or, on the second path, the Delete affordance does not appear here.

    ⇒ Fixes #10383.

    Amended by the seat (review of PR objectui#10424 at ea0c5c627)

    The first head copied the console list's delete steps into plugin-view. That is a second implementation of the same operation, and it already diverged on consent: the ADR-0094 package-owned permission-set reset question and toast. The seat rule for one operation with two diverging implementations: the governed side wins, and the other re-binds to it. ⛔ It is not aligned by copying and not double-written. So the respin moves the console's delete core into one shared helper and binds both paths to it. The helper is a new export, so Clause-② is now yes, and a contract review runs before landing.

    domain:ui seat #1 · session_01BA3nKVUwKQJf8DBxrSVtNC · claim · 2026-09-24T23:40Z

  5. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 10383,
    "status": "done",
    "branch": "claude/issue-10383-object-view-delete-does-nothing",
    "pr": "#10424",
    "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — mode:subagent, the parent PM seat's id (container CLAUDE_CODE_REMOTE_SESSION_ID cse_01BA3nKVUwKQJf8DBxrSVtNC); matches the claim 5824094305",
    "premise_still_valid": true,
    "summary": "Premise confirmed on origin/main a707197: ObjectView.handleDelete / handleBulkDelete took an unused record argument and only bumped refreshKey, and ObjectGrid hands the row to onDelete / onBulkDelete without deleting (its own comments say the consumer owns confirm + toast + refresh). A temporary probe through the REAL ObjectGrid reproduced it before any edit: row Delete and bulk Delete each called dataSource.delete 0 times, bulkDelete 0, bulk 0, and the row was still shown. Road 1 was taken because it matches the console list. The console list (app-shell ObjectView, then ListView, then the same ObjectGrid, with handlers from useObjectActions) confirms through ActionConfirmDialog (actionConfirm.* chrome; objectActions.deleteConfirm for a row, console.objectView.bulkDeleteConfirm once for a selection), calls dataSource.delete per record (Promise.allSettled for several; it does NOT use the optional DataSource.bulkDelete), and afterwards refreshes and toasts deleteSuccess / bulkDeleteSuccess / bulkDeletePartial; a failed single delete toasts deleteFailed with the error message and does not refresh. Its handlers read no permission: the affordance gate is ObjectGrid's own (operations.delete, perms.can(object,'delete'), bucket/userActions/API operations, per-record explain), the same component on both paths. The plugin-view handlers now repeat those steps with a local AlertDialog in the same shape and keys, plus toast, both from @object-ui/components. The nine i18n keys already exist in all ten packs; English defaults were added to VIEW_DEFAULT_TRANSLATIONS, byte-identical to en. What did not change: no new dependency, no export or signature change (Clause-② no holds), no edit to ObjectGrid.tsx / RowActionMenu.tsx / app-shell / plugin-list, and the grid remount-to-refresh is untouched. Zone-2 readings: #1 confirmed; #2 reproduced; #3 as above; #4 plugin-view -> components/i18n are existing dependencies and nothing comes from app-shell; #5 DataSource declares optional bulkDelete(resource, ids) and bulk?(), and the loop matches the console; #6 hidden where the principal may not delete, by ObjectGrid, now pinned on this path; #7 only the grid branch receives handleDelete, and the kanban/calendar/gantt/other branches get none, so none shares the no-op. Assignee was os-bill (the PM's) at start and was not touched.",
    "tests": "All at head ea0c5c6 unless noted. (1) Pin: packages/plugin-view/src/tests/ObjectView.gridDelete-10383.test.tsx, 7 cases driving the real ObjectGrid through ObjectView against an in-memory data source: row confirm then delete(test_object, r1) and the row is gone; Cancel deletes nothing; a refused delete shows 'Failed to delete Test object' with the message and the row stays; bulk confirm 'Delete 2 selected records? This cannot be undone.' then 2 delete calls, 0 bulkDelete, and both rows are gone; partial failure shows '1 deleted, 1 failed' and still refreshes; a permission pair where the grant shows row and bulk Delete, and no grant shows an open menu with Edit and no Delete and no bulk Delete. pnpm exec vitest run of that file: Tests 7 passed (7). (2) Reverse verification with the fix already committed: objectstack scripts/ablation-replace.mjs in WRAP mode put the two handlers back to their refresh-only bodies (anchor x1 -> x0, blob f011d9f8 -> 566dcc9a). Pre-registered 5 red / 2 green; observed 'Tests 5 failed | 2 passed (7)'. The 2 green are the permission pair, which pins ObjectGrid's gate and not the handlers. Restore proven: blob after restore == HEAD blob f011d9f8 and git diff HEAD empty. There is no dist in the path: the test imports ../ObjectView, and plugin-grid resolves to src through the root vitest alias. (3) turbo run build --filter='@object-ui/plugin-view^...' --concurrency=2: 15/15 successful. (4) pnpm --filter @object-ui/plugin-view run type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0 at ea0c5c6; --listFilesOnly shows the new test in the test program (1 hit). (5) pnpm exec vitest run packages/plugin-view/ --maxWorkers=2 from root: Test Files 48 passed (48), Tests 440 passed (440), on a tree byte-identical to ea0c5c6. (6) Console registered-path suites plus text readers of the touched sources, one run: Test Files 23 passed (23), Tests 1298 passed (1298). Registered-path suites: StudioDesignSurface.gridRefresh, studio-canvas-preview, StudioDesignSurface.studioCanvasLeaf, rowClickModifierPayloadForward-9462, apps/console timelineAxisRefusalReach-7070, schema-catalog catalog-gallery-render. Text readers: defaults-maps-mirror-en-pack, fallback-placeholder-spelling-3512, objectView-config-keys-retired-4730, raw-key-call-sites-3546, relayRungCensus-7559, titleFieldConvergence, onRowClickArity-9357, types object-kanban-group-by-limit-7322 / object-view-unmirrored-keys-7779 / schema-reference-named-list-view-keys-7923 / tree-view-config-readers-8253 / zod-mirror-parity, column-identity.ratchet, flow-envelope.ratchet, scripts check-handler-key-read-sites / check-lucide-icon-record-names / check-i18n-call-site-keys. The in-package readers objectViewHostSurface and modifierClickNewTab-9806 ran in (5). (7) Gates at ea0c5c6: check:control-bytes OK (8473 files); check:new-line-citations VERDICT 0 new, exit 0; check-changeset-presence 1 released package, 1 changeset, exit 0; check:changeset-claims exit 0, listing 5 pending changesets that name plugin-view/src/ObjectView.tsx (6726, 7070, 7499, 8653, 7779), each re-read and still true because none describes the delete handlers; check:i18n-keys, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:i18n-dead-keys all exit 0. My own control-byte grep over the 3 touched files found 0 hits. (8) eslint --no-inline-config --format json, per-rule counts compared with the base (base content fed through --stdin-filename at the same path): ObjectView.tsx base == head (no-explicit-any 53 warn, only-export-components 4 warn, exhaustive-deps 3 warn); the new test has 0 messages. Governed-surface guard --test: NOT GOVERNED. Lock: the first full-suite attempt returned VERDICT queue-timeout exit 99 (NOT MEASURED; holder was another worktree's app-shell run). The same OS_VERIFY_LOCK_SLOT was re-taken and the suite measured green in (5).",
    "mcp_calls": "0 — no MCP GitHub tool was called (reads went through REST GET via the proxy)",
    "api_writes": "2 fleet-relay strokes, each = 1 POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write run as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectui/pulls (draft forced) = PR 10424, run 36077024661 success; (2) this os-dev-report -> POST /repos//issues/10383/comments. Plus git push of the branch (not REST). Zero label writes, zero PR-body PATCHes.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — ADR-0094 copy is console-only. For a package-owned sys_permission_set row, useObjectActions asks resetPackageSetConfirm and toasts resetPackageSetSuccess; on this path the same row now gets the generic delete copy (before this change the path deleted nothing at all). Porting it would put a system-object special case into a generic plugin or need a shared lower-package helper, so it stays outside this card's diff. Where it applies here: e.g. the Studio object canvas (StudioObjectRecordsCanvas renders the registered object-view) opened on sys_permission_set. Dedupe words: object-view permission set reset copy · ADR-0094 plugin-view delete · resetPackageSetConfirm registered path",
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — observation, not a filing class: plugin-detail DetailView.handleDelete comments that the ActionProvider onConfirm 'will intercept' window.confirm; nothing intercepts a native window.confirm (comment accuracy, no user-visible failure)",
    "conflict noted, not chosen silently: the harness attribution reminder asked for a Co-Authored-By trailer naming a model on commits. The dispatch order says commit trailers are model-free only, and the agent definition says the dispatch contract wins. Commits carry only the model-free Claude-Session trailer. One unpushed commit was amended before its first push to drop a card reference line; no pushed history was rewritten"
    ]
    }

  6. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT — PR objectui#10424 at 473c86933 · contract review FAIL on the PR body only → fixed by the seat's body patch · entering the merge queue

    domain:ui seat #1, session_01BA3nKVUwKQJf8DBxrSVtNC. The claim (amended 5824094305) says Clause-②: yes: @object-ui/core gains one export, recordDelete. A review-tier contract review ran against the card, the amended claim and the PR only. Its record is below, verbatim.

    Round 1 → round 2.

    • The record passes every derived judgment (①1–①6) and the semver level (②). Its one FAIL is the PR BODY: it still declared Clause-②: no, "no new export", "ADR-0094 copy is not ported", and gates at ea0c5c627. That item needs no code change, and the head does not move.
    • The seat patched the body at 473c86933: line 2 now reads Clause-②: yes; the fix is described as the shared core; the pins, both ablation legs and the gates are stated at this head; the ADR-0094 note is inverted.
    • The seat re-read the patched body against the diff and the record, and item 1 is closed. ⇒ PASS.

    Why the respin (the seat's review of the first head, ea0c5c627). The first head copied the console's delete steps into plugin-view. That is one operation with two implementations, and the copy already diverged on consent (the ADR-0094 reset question). Under the seat's rule — the governed side wins and the other re-binds; ⛔ no copying, ⛔ no double-writing — the console's delete core moved into recordDelete in @object-ui/core, and both hosts now bind to it. The console's behaviour is byte-identical (its hook test is unchanged).

    Maintainer veto window (the seat does not hold landing for these; they are named in the round report):

    1. On the registered object-view path, grid Delete now deletes. For a package-owned sys_permission_set row it asks the RESET question and shows the reset toast, the same as the console.
    2. packages/core/README.md does not document recordDelete. objectui AGENTS.md Add automated testing infrastructure and CI/CD workflows #2 read literally asks for it. Standing practice is silence (no sibling action export is documented there), and no gate enforces it, so the seat leaves it to your call. A README paragraph can ride a follow-up.

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 473c86933be38cf4c768aff52acf6d94df51c544

    Inputs read in full: objectui#10383 body; comments 5822484555 (triage grading), 5823930334 (its correction), 5824094305 (claim, including the "Amended by the seat" section), 5824519657 (first os-dev-report, measured at ea0c5c627); PR #10424 body as served by REST at review time (updated_at 2026-09-25T01:23Z); git diff origin/main...473c86933 over all 9 files (.changeset/10383-object-view-grid-delete-deletes.md, .changeset/10383-record-delete-core.md, packages/app-shell/src/hooks/useObjectActions.ts, packages/core/src/actions/__tests__/recordDelete.test.ts, packages/core/src/actions/index.ts, packages/core/src/actions/recordDelete.ts, packages/plugin-view/src/ObjectView.tsx, packages/plugin-view/src/__tests__/ObjectView.gridDelete-10383.test.tsx, scripts/__tests__/check-i18n-call-site-keys.test.ts) plus the head copies of the three source files and both test files; origin/main:packages/app-shell/src/hooks/useObjectActions.ts in full; the seat rule verbatim from objectstack .claude/skills/pm-dispatch/SKILL.md §升级与决策 (lines 226-227: 「② 一个操作两个实现且行为不一致 ⇒ 带治理的一侧(权限闸、同意、去重、审计)胜出。」「另一侧改绑并删除,不是对齐也不是双写;反向裁只在产品语义明确要求时成立。」); ADR-0094 as it appears in the code (the [ADR-0094] blocks in useObjectActions.ts on origin/main and the module docblock of recordDelete.ts) and the delete rows of objectstack docs/adr/0094-sys-permission-set-pure-projection.md (lines 144-145: delete of an artifact-backed set = overlay tombstone = reset); objectui AGENTS.md #2 (docs-driven, line 94), the changeset-presence rule (lines 167-169) and the version policy (lines 258-262); objectstack AGENTS.md lines 1084-1096 (Clause-② declaration, 「yes takes at least minor」); pm-dispatch references/landing-operations.md lines 5-20 and references/contract-review.md; the console callers packages/app-shell/src/views/ObjectView.tsx 2971-2993; packages/core/src/actions/ActionRunner.ts 1176-1183, 1236-1272, 1328-1370; packages/plugin-grid/src/ObjectGrid.tsx 1504-1530, 1580-1600, 1662-1715, 4003-4050; packages/i18n/src/useSafeTranslation.ts; packages/types/src/data.ts 476-482; the head's 43 check-runs via REST.

    Measurement notes. A scratch worktree at the head under <scratchpad>/pr-10424/wt (offline install, removed after this record): pnpm exec vitest run over the 9 suites named in ①2-①5 → Test Files 9 passed (9), Tests 211 passed (211); pnpm run type-check in packages/core, packages/plugin-view, packages/app-shell → exit 0 each (against a freshly built packages/core/dist, which carries dist/actions/recordDelete.d.ts re-exported through dist/actions/index.d.ts); node scripts/check-changeset-presence.mjs → "6 source file(s) of 3 released package(s) changed, and this change declares 2 changeset(s)", exit 0; check-changeset-no-major / check-changeset-fixed / check-pending-changeset-literals → OK; node scripts/check-i18n-call-site-keys.mjs → exit 0 ("Interpolation parity: 2807 call sites … every call site passes exactly the arguments that value has holes for"; the scanner walks packages and apps, line 1406, so packages/core is in scope). Head check-runs: 43 total, 40 success, 3 skipped (coverage shards and dependabot), 0 failures; the green set includes Type Check, Test (shard 1/8 … 8/8), Test (dist pins), Changeset Declaration, Changeset Bump Policy, Changeset Claim Re-read, Changeset Fixed Group Check, README Export Check, Governed Surface Queue Guard, Line Citation Gate, Lint. The PR body's own "Gates" section and the only os-dev-report on the card were measured at the pre-respin head ea0c5c627; nothing on the card measures 473c86933, so the readings above are this record's.

    ① Derived judgments

    1. The new export recordDelete. Reachable: packages/core/src/index.ts:52 export * from './actions/index.js' → packages/core/src/actions/index.ts gains exactly one line, export * from './recordDelete.js'. Exactly one name: recordDelete.ts has one export statement (line 109, export const recordDelete = Object.freeze({ confirmText, run })); RecordDeleteDeps, RecordDeleteRequest, Translate, Row, PERMISSION_SET_OBJECT, isPackageOwned are module-private, so export * publishes one value and nothing else (confirmed on the built dist/actions/recordDelete.d.ts). Signature minimal: confirmText(deps: Pick<Deps,'objectName'|'t'>, record?) and run(deps, request); deps = objectName, label, t, dataSource{delete, findOne}, toast{success, error}, onRefresh? — each one is read by run and nothing unused is asked for; request is the runner's own action shape so the console registers run unchanged. Sufficient for both callers: app-shell passes { objectName, label: objectLabel || objectName, dataSource, t, toast (sonner), onRefresh } (head useObjectActions.ts:95-99), plugin-view passes { objectName: schema.objectName, label: objectSchema.label || objectName, dataSource, t: tView, toast (@object-ui/components, a sonner re-export), onRefresh: () => setRefreshKey(+1) } (head ObjectView.tsx:2602-2613); DataSource.delete(resource, id, opts?) : Promise<boolean> (types/src/data.ts:478-482) and findOne are structurally assignable, and both hosts type-check. No new dependency: recordDelete.ts imports only type { ActionResult } from './ActionRunner.js'; packages/core/package.json is untouched (deps: @object-ui/types, @objectstack/formula, @objectstack/spec). Lowest common package, no inverted edge: @object-ui/core is a direct dependency of both @object-ui/app-shell and @object-ui/plugin-view (their package.json at head), sits below @object-ui/react (which itself depends on core; the claim had named react first — core is lower and already shared), and core imports nothing from either host or from i18n/sonner (the translator and the toast sink are injected). PASS

    2. Console re-bind — behaviour-identical to origin/main. Line-by-line against origin/main:useObjectActions.ts 86-181 and 203-221: the records filter (Array.isArray(params.records) ? filter(r?.id != null) : null), the length > 1 bulk branch (Promise.allSettled(records.map(r => dataSource.delete(objectName, r.id))) — the head adds only an as string cast, no conversion — then onRefresh?.() BEFORE the toast, t('objectActions.bulkDeleteSuccess', { count, label, defaultValue: \Deleted ${n} ${label} records` })→{ success: true, reload: true, silent: true }, else t('objectActions.bulkDeletePartial', { succeeded, failed, defaultValue })→{ success: false }with noerrorkey), the id chainparams.recordId ?? params.record?.id ?? records?.[0]?.id ?? request.recordId, the noRecordIdreturn{ success: false, error: t('objectActions.noRecordId') }, the ADR-0094 block (objectName === 'sys_permission_set', params.record.managed_by !== undefined ? params.record : await dataSource.findOne(objectName, recordId), managed_by === 'package', wrapped in try/catch falling back to the generic copy), the single try (delete→onRefresh→toast.success(reset ? t('objectActions.resetPackageSetSuccess', { defaultValue }) : t('objectActions.deleteSuccess', { label }))→{ success: true, reload: true, silent: true }) and catch (toast.error(t('objectActions.deleteFailed', { label }), { description: err.message })→{ success: false }, no refresh) are the same statements in the same order with the same keys, args and defaultValues; labelis bound toobjectLabel || objectNameexactly where main computed it inline.deleteRecord: main's packagedSetReset = objectName === 'sys_permission_set' && record?.managed_by === 'package'choosingresetPackageSetConfirm {defaultValue}vsdeleteConfirmisrecordDelete.confirmText({ objectName, t }, record)with the same two calls and the samedefaultValuestring;params: record ? { recordId, record } : { recordId }and the deps[execute, t, objectName]are unchanged; theuseEffectdeps array is unchanged. The handler is still a promise-returning function registered under'delete', and the runner still awaits it and catches a rejection (ActionRunner.ts:1240-1243, 1268-1272), so a throw anywhere is converted the same way as before. Truth table (identical in main and head; n= rows withid != null` after the filter):

      request branch id used ADR-0094 row source (only sys_permission_set) return
      params.records, n>1 bulk allSettled, refresh, one toast each r.id raw not consulted {success:true,reload:true,silent:true} / {success:false}
      params.records, n=1 single records[0].id params.record absent → findOne single-path returns
      params.records, n=0 single falls through to params.recordId ?? params.record?.id ?? request.recordId — {success:false,error:noRecordId} if none
      params.record (row) single record.id the row if it carries managed_by, else findOne single-path returns
      params.recordId (+ optional record) single params.recordId as above single-path returns
      legacy top-level recordId single request.recordId findOne single-path returns
      none single undefined — {success:false,error:t('objectActions.noRecordId')}, no toast

      Single-path returns: success → dataSource.delete, onRefresh, success toast, {success:true,reload:true,silent:true}; failure → error toast with description: err.message, NO refresh, {success:false} (no error, so handlePostExecution at ActionRunner.ts:1359 toasts nothing a second time). useObjectActions.test.tsx unchanged: git diff --quiet origin/main 473c86933 -- packages/app-shell/src/hooks/__tests__/useObjectActions.test.tsx exits 0; it mocks sonner (line 44) and does not mock @object-ui/core, so it exercises the real core through the hook, and it passes at head (7 cases: toast de-duplication ×3, ADR-0094 reset copy ×3 including the findOne fallback for { recordId } only, plus setup). PASS

    3. plugin-view re-bind. performDelete is gone: git grep performDelete 473c86933 -- packages apps scripts → 0 hits (it existed only at ea0c5c627). The one-row question is recordDelete.confirmText({ objectName: schema.objectName, t: tView }, deleteRequest?.records[0]) and Continue calls recordDelete.run(...) (head ObjectView.tsx:2589-2593, 2600-2616); the host owns only the actionConfirm.* chrome and the bulk question tView('console.objectView.bulkDeleteConfirm', { count }). Request shapes match the console's byte for byte: console row actions.deleteRecord(String(record.id), record) behind if (record?.id != null) → params: { recordId, record } (app-shell/src/views/ObjectView.tsx:2971-2977, useObjectActions.ts:132); plugin-view row handleDelete skips record?.id == null and sends { params: { recordId: String(records[0].id), record: records[0] } }. Console bulk valid = records.filter(r?.id != null); if (valid.length === 0) return; params: { records: valid } with the bulk question over valid.length (:2980-2993); plugin-view handleBulkDelete filters the same way, returns on empty, and sends { params: { records } } with the bulk question over records.length. A package-owned sys_permission_set row now gets the reset question and the reset toast on this path: pinned by the new test's last describe (dialog text = the full resetPackageSetConfirm sentence; successSpy = 'Permission set reset to its shipped baseline'; findOne not called because the row was passed), with the environment-owned control. Cancel: AlertDialogCancel closes via Radix → onOpenChange(false) → setDeleteRequest(prev => ({ ...prev, open: false })), the request is retained (so the description does not blank during the exit animation) and run is never called; pinned ("Cancel deletes nothing and the row stays"). Double-click guard: AlertDialogAction is AlertDialogPrimitive.Action (components/src/ui/alert-dialog.tsx:109-111), so the first click runs if (!deleteRequest?.open) return; with open: true, sets open: false, starts run, and Radix closes; React flushes that discrete event synchronously, so the still-mounted exit-animating content re-renders with a closure whose deleteRequest.open === false, and a second click returns at the guard. Judged from the code; the pin suite does not exercise the animation window (non-blocking, see ③).
      The dev's two edges. (a) Bulk ids forwarded raw: main's console handler already did dataSource.delete(objectName, r.id) raw on the bulk path, and both hosts stringify only on the row path before the core (String(record.id) / String(records[0].id)); so the core's "ids forwarded exactly as supplied" is the console's existing behaviour, and the row ids come from the same grid on both paths. Not a divergence; does not block. (b) A synchronous throw inside the bulk map rejects run: DataSource.delete is declared Promise<boolean> and every in-tree implementation is async (data-objectstack/src/index.ts:3940) or returns Promise.reject(...) (plugin-form/src/EmbeddableForm.tsx:329), so no in-tree adapter throws synchronously; on the console the runner's catch converts it exactly as it did for main's inline handler; on plugin-view void recordDelete.run(...) would surface as an unhandled rejection with no toast and no refresh. Unreachable in-tree, same class as main on the governed side, and the hosts share the one core; does not block. A .catch on the void run(...) call (or moving the bulk map inside the try) is a one-line hardening for a follow-up. PASS

    4. The i18n pin re-point. The tuple [file, absent, present] moved from packages/app-shell/src/hooks/useObjectActions.ts to packages/core/src/actions/recordDelete.ts; present ("t('objectActions.resetPackageSetSuccess', {") is unchanged and matches head line 200; absent ("t('objectActions.resetPackageSetSuccess', {\n label:", 14 spaces) is the indentation of the new file (the defaultValue: line at 201 sits at 14 spaces, so a reintroduced label: in first position is caught, as the 16-space form was in the old file). The test still proves the inert label is absent from resetPackageSetSuccess (not.toContain(absent)) and adds a new sister assertion, expect(recordDeleteCore).toContain("t('objectActions.deleteSuccess', { label })"), beside the existing marketplace.install.updateTo sister; the repo-wide parityOf(repoRoot) assertion in the same file is untouched and green at head, and the mechanized parity gate (check:i18n-keys) covers packages/core. Strengthened, not weakened. PASS

    5. No reader of the retired inline copies or of the old refresh-only behaviour. git grep -n "performDelete\|resetPackageSetConfirm\|resetPackageSetSuccess\|packagedSetReset" 473c86933 -- packages apps scripts (locales excluded) → only recordDelete.ts, recordDelete.test.ts, the re-pointed i18n pin, check-i18n-en-drift.{mjs,test.ts} (key-spelling comments, not readers of the hook) and CHANGELOG prose. No test or script pins handleDelete = useCallback((_record or the refresh-only body (git grep "_record: Record<string, unknown>" → one unrelated TS-error quotation in react/src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx:128). The plugin-view suites that read refreshKey (ObjectView.expandGate, ObjectView.refreshInPlace-10035, ObjectView.refreshSignal) pass at head; check-changeset-claims (the five pending changesets naming ObjectView.tsx) exits 0. PASS

    6. Accept set and public surface. Widens by exactly one value export on @object-ui/core (recordDelete), reachable from . — the exports map and every package.json are untouched. Nothing narrows: useObjectActions's exported ObjectActions shape, deleteRecord(recordId, record?), and plugin-view's ObjectViewProps are unchanged; no authorable key is added or re-read; no i18n key is added (all nine objectActions.*/actionConfirm.*/console.objectView.bulkDeleteConfirm keys plus resetPackageSetConfirm/resetPackageSetSuccess exist in all 10 packs; the en values added to VIEW_DEFAULT_TRANSLATIONS are byte-identical to en.ts, and defaults-maps-mirror-en-pack passes). The permission gate is unchanged and decides both paths: plugin-view hands onDelete/onBulkDelete only when operations.delete !== false (ObjectView.tsx:2461-2462), and ObjectGrid — untouched by this diff — conjoins operations.delete, perms.can(objectName,'delete') (ObjectGrid.tsx:1513), the object's managedBy/userActions/effectiveApiOperations through resolveRowCrudAffordances (:1664-1690), the per-record explain verdict (:1704-1711, :3807) and the bulk objectCanDelete/canDelete && onBulkDelete verdict (:4017-4048); the console's own handlers read no permission either (origin/main hook, confirmed). Pinned on this path by the permission pair in the new suite (grant → row and bulk Delete; no grant → Edit only, no bulk Delete). PASS

    ② Semver level

    Changesets present: .changeset/10383-record-delete-core.md ('@object-ui/core': minor) and .changeset/10383-object-view-grid-delete-deletes.md ('@object-ui/plugin-view': minor); none for @object-ui/app-shell. Consistent: objectstack AGENTS.md line 1085 — yes takes at least minor — is met by the core entry; objectui's changeset-presence rule is per change, not per package (check-changeset-presence.mjs header: "the same change must declare a .changeset/*.md"; measured at head: 3 released packages changed, 2 changesets declared, exit 0; CI Changeset Declaration green), and app-shell's user-visible behaviour is unchanged (①2), so it has nothing to declare; all three packages sit in the one fixed group (40 packages), so the family takes one minor bump regardless. No major (check-changeset-no-major OK; CI Changeset Bump Policy green), as objectui AGENTS.md lines 258-262 require. plugin-view minor for a behaviour change is within "at least" (a fix may take patch under objectstack's rule; the family bumps minor through core anyway).
    Prose against the diff. plugin-view entry: every sentence holds — the defect description matches origin/main (_record ignored, setRefreshKey only), "bind to the same record-delete core as the console's own object list (recordDelete from @object-ui/core)", the row question / reset question / bulk-once question, dataSource.delete(objectName, id) per record then refresh, Cancel no-op, the four toasts including "no refresh" on a failed row delete, the dialog being ObjectView's own in the console's shape, the keys existing in every pack, and the affordance verdict list (operations.delete, grant, lifecycle and userActions, API operations, per-record verdict) matching ObjectGrid. core entry: the two-method surface, the injected deps and the "no i18n or toast dependency" claim, "no path returns an error except a request with no record id", the ADR-0094 paragraph (row read first, best-effort findOne), and "The console's behaviour is unchanged: its handler code moved here with no behaviour change, including the returns that keep the runner from showing a second toast" all match ①1-①2. Two non-blocking imprecisions: "performs the delete and reports it: … then a refresh and the success, failure or 'N deleted, M failed' toast" reads as if a failed single delete also refreshes — it does not (the plugin-view entry says so correctly); and the request parenthetical (params.records, or params.recordId with an optional params.record) omits the params.record-alone and legacy top-level recordId shapes the code (and the unit test) accept. Neither changeset carries a Clause-② line; 17 of 1955 existing changesets do (closest precedent 10183-date-only-shared-parse-step.md, "core gains one export") — a precedent, not a rule. PASS

    ③ Boundary flags

    • packages/core/README.md does not document recordDelete. objectui AGENTS.md Add automated testing infrastructure and CI/CD workflows #2 reads literally ("For every feature/refactor, update package README.md and content/docs/guide/*.md. Not done until docs reflect the code."), and this PR is both. Against that: the core README's API section defers to external docs ("See full documentation … for detailed API reference"), none of the sibling action-module exports is documented there (recordIdParam, bulkFastPath, UndoManager, TransactionManager, actionKeys, ActionEngine, actionErrorDetail → 0 README hits each; TransactionManager alone appears in one guide page), and the README Export Check gate enforces only the README→code direction (import examples name real exports). So the rule is not mechanized for this direction and the standing practice is silence; a short "Record delete (recordDelete)" paragraph beside "Server Action Dispatch" would satisfy Add automated testing infrastructure and CI/CD workflows #2 at the cost of a few lines. Non-blocking; maintainer's call whether it rides this PR or a follow-up. The plugin-view README's "All four operations … run against the dataSource prop" became true for Delete with this PR (the dev's note), and needs no edit.
    • plugin-view resolving console.objectView.bulkDeleteConfirm (a console.* key) from a plugin. Precedent already on main: plugin-view reads console.objectView.new (ObjectView.tsx:255) and plugin-list reads nine console.objectView.viewType* keys (ViewSwitcher.tsx:106-141); the key exists in all 10 packs, and the provider-less path has its en value in VIEW_DEFAULT_TRANSLATIONS. Reusing the console's key is what makes the two paths read the same in every language, which is the point of the re-bind; moving the key to a shared namespace would be a separate i18n card touching ten packs and the console. Non-blocking.
    • The PR body is stale at this head and contradicts it on the very surface under review. It still declares Clause-②: no, states "No new export and no changed exported signature, so Clause-② no holds", says "ADR-0094 copy is not ported" (it is, through the shared core — the plugin-view changeset and the new test say so), lists the gates at ea0c5c627, and names only the plugin-view changeset. The claim comment 5824094305 — the declaration limb the enqueue gate reads (landing-operations.md: 「声明肢 = 认领评论声明 Clause-②: yes」, 「错误的 no 是可审计的假申报」) — was amended to yes, and no objectui CI job reads the body's line (Changeset Bump Policy runs check-changeset-no-major only; the body-reading Check Changeset gate is the platform repo's). The bytes are right; the PR's durable record is wrong. This is the one item held as FAIL below because it is the contract declaration itself; the fix is a PR-body edit, not a code change, and does not move the head this record is keyed on.
    • Other things the maintainer should know. (a) No os-dev-report on the card measures 473c86933; the head's 43 check-runs are green (40 success, 3 skipped), and the local re-measurements in the notes above agree. (b) recordDelete.confirmText(...) is evaluated on every ObjectView render as a JSX child even when no dialog is open (it returns the plain question for an undefined row); pure and cheap, noted only. (c) The exit-animation double-click guard is judged from the code (①3), not pinned; an optional follow-up case could click Continue twice within the close window and assert one delete call. (d) With an adapter that emits onMutation, a delete refreshes the grid twice (handler bump + subscription), as the dev noted; the console list behaves the same with its realtime refresh. (e) void recordDelete.run(...) in plugin-view has no .catch; unreachable in-tree (①3b) but a one-line hardening.

    Verdict

    FAIL — one item, no code change required:

    1. PR fix(plugin-view): the grid's row and bulk Delete delete on the registered object-view path #10424 body (not the diff): Clause-②: no and the sentences that rest on it. At 473c86933 the diff adds a public export on @object-ui/core, so the body's Clause-②: no, the bullet "No new export and no changed exported signature, so Clause-② no holds", the acceptance note "ADR-0094 copy is not ported", the "Gates" section measured at ea0c5c627, and the single-changeset line are false for this head. Minimal fix: PATCH the PR body so its line-initial declaration reads Clause-②: yes (or Clause-②: yes (widening)), replace the "No new export" bullet with the one export recordDelete and the two changesets (@object-ui/core: minor, @object-ui/plugin-view: minor), drop or invert the "ADR-0094 copy is not ported" note, and either re-measure the Gates section at 473c86933 or say it was measured at ea0c5c627 and point at the head's check-runs. The head sha does not move, so ①-③ above stand once the body is corrected; no patch round on code is asked for.

    Every derived judgment (①1-①6) and the semver level (②) PASS on the diff as it stands.

    Seat's round-2 note

    Verdict item 1 (the PR body) is fixed by the seat's body patch at 473c86933; the head is unchanged. The record's non-blocking flags are carried as follows:

    • The core README → the maintainer veto window above.
    • .catch on void run(...), the double-click-guard pin, and the changeset prose imprecisions → PR Acceptance notes. 承接者:无.
      ⇒ PASS, and the PR enters the merge queue.

    domain:ui seat #1 · review · 2026-09-25T01:45Z

  7. added a commit that references this issue on Sep 28, 2026
    ff14e29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions