Repository navigation
fix(plugin-view): the grid's row and bulk Delete delete on the registered object-view path - #10424
Conversation
…egistered object-view path On the registered `object-view` renderer (no host list view), the grid's row Delete and bulk Delete only bumped the refresh counter: `ObjectGrid` hands the row to the consumer's `onDelete` / `onBulkDelete` and performs no delete itself, so a Delete offered by default called `dataSource.delete` zero times and the row came back. The two handlers now do what the console's own list does with the same callbacks: confirm (`objectActions.deleteConfirm` for a row, `console.objectView.bulkDeleteConfirm` once for a selection, in the `actionConfirm.*` dialog chrome), then `dataSource.delete` per record, refresh, and the console's success / failure toasts. Whether the affordance is offered stays `ObjectGrid`'s verdict, as on the console list. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…in-view both bind to The first repair of the plugin-view grid Delete copied the console's delete steps into a second flow, and the copy already diverged on consent: it asked a package-owned `sys_permission_set` row the plain delete question, where the console asks the ADR-0094 reset question and toasts the reset. `recordDelete` (`@object-ui/core`, the lowest package both hosts already depend on) is now the one copy: `confirmText` chooses the one-record question, and `run` performs the single delete (with the ADR-0094 detection and its `findOne` fallback) or the settled bulk delete, and reports it. The translator, toast sink, data source and refresh are injected, so core gains no i18n or toast dependency. - app-shell `useObjectActions`: the `delete` handler and `deleteRecord` call the core; the moved code is unchanged, including the `silent` / no-`error` returns that stop the runner's double toast. - plugin-view `ObjectView`: `performDelete` and the local question are gone; its AlertDialog asks the core's question and Continue runs the core, so a package-owned permission set now gets the reset copy here too. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…-delete core `resetPackageSetSuccess` moved, unchanged, from app-shell's `useObjectActions.ts` into `@object-ui/core`'s `recordDelete`, so the pin that proves its inert `label` argument stays deleted now reads that file, and it also pins the sister `deleteSuccess` call that still passes its `label`. The core import in `useObjectActions.ts` is folded into the existing `@object-ui/core` import line, so no line below it moves; that keeps the `useObjectActions.ts:21` citation in `one-authority-per-exported-name-6273.test.ts` true. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…tate the plugin-view entry `@object-ui/core` gains the `recordDelete` export, so it gets a minor bump. The plugin-view entry now says the grid Delete binds to that shared core, so a package-owned permission set gets the reset question and toast on this path too. No app-shell changeset: the console's delete behaviour is unchanged, and check-changeset-presence does not require one. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Fixes #10383
Clause-②: yes
What was wrong
On the registered
object-viewrenderer path (ObjectViewwith no hostrenderListView),ObjectGridhands the clicked row, or the selection, toonDelete/onBulkDeleteand performs no delete of its own: its own comments say the consumer's delete flow "owns the confirmation, the toast and the refresh".ObjectView'shandleDelete/handleBulkDeleteignored their argument and only bumpedrefreshKey. Delete is offered by default (operations.deletedefaults to true), so the click deleted nothing and the row came back after the refresh.Reproduced on
origin/maina70719765before any edit, through the REALObjectGrid(a temporary probe, never committed): row Delete and bulk Delete each gavedataSource.delete0 calls,bulkDelete0,bulk0, and the row was still shown (AssertionError: expected 0 to be greater than 0, 2 of 2 failed).Measured first: how the console's own list deletes (read only)
app-shellobject page rendersListView, which forwardsonDelete/onBulkDeleteto the sameObjectGrid.actions.deleteRecord(String(record.id), record), skipping a row with noid, which runsexecute({ type: 'delete', confirmText: t('objectActions.deleteConfirm'), params }). The confirm goes through the console runtime'sconfirmHandler, which opensActionConfirmDialog: a Shadcn AlertDialog titledactionConfirm.title, buttonsactionConfirm.cancel/actionConfirm.confirm, the question as the description.id, thenexecute({ type: 'delete', confirmText: t('console.objectView.bulkDeleteConfirm', { count }), params: { records } }), so there is one confirm for the batch.useObjectActions' registereddeletehandler. More than one record:Promise.allSettled(records.map(r => dataSource.delete(objectName, r.id))). One record:dataSource.delete(objectName, recordId). It does not use the optionalDataSource.bulkDelete.objectActions.bulkDeleteSuccessorobjectActions.bulkDeletePartial. Single: on success it refreshes and toastsobjectActions.deleteSuccess; on failure it toastsobjectActions.deleteFailedwith the error message as the description, and does not refresh.ObjectGrid:operations.delete, AND the principal'sperms.can(object, 'delete'), AND the object's bucket /userActions/ effective API operations, AND the per-record explain verdict. It is the same component on both paths, so it is the same gate.The fix: ONE record-delete core, and both hosts bind to it
The first head (
ea0c5c627) copied the console's delete steps intoplugin-view. The seat sent it back: that was a second implementation of the same operation, and it already diverged on consent. It asked a package-ownedsys_permission_setrow the plain delete question, where the console asks the ADR-0094 reset question. The seat's rule for one operation with two diverging implementations: the governed side wins and the other re-binds to it. ⛔ No copying, ⛔ no double-writing.recordDeletein@object-ui/core(packages/core/src/actions/recordDelete.ts, exported throughactions/index.ts). It is the one new public export (henceClause-②: yes): a frozen{ confirmText, run }.confirmText(deps, record?)picksobjectActions.deleteConfirm, orobjectActions.resetPackageSetConfirmfor asys_permission_setrow withmanaged_by: 'package'.run(deps, request)is the console's former inlinedeletehandler, moved statement for statement: the id chain, the single delete with the ADR-0094 detection (the row first, then a best-effortfindOne), the bulkallSettled, the four toasts, and the runner-shaped returns (silenton success, noerrorkey, so no double toast).t,toast,dataSource,onRefreshand the label are INJECTED, so core gains no i18n or toast dependency. Core is the lowest package both hosts already depend on: no new dependency edge, and nothing is inverted.app-shelluseObjectActions): thedeletehandler isrecordDelete.run(...), anddeleteRecord's question isrecordDelete.confirmText(...). The behaviour is byte-identical;useObjectActions.test.tsxis unchanged (a 0-line diff) and green.ObjectView):performDeleteand the local question are gone. TheAlertDialogstays as this host's confirm UI (actionConfirm.*chrome, the exit-animationopenguard). The question and the delete come from the core, with the same request shapes the console hands its runner. So a package-owned permission set on this path now gets the reset question and the reset toast.ObjectGrid's verdict on both paths (operations.delete, the delete grant, the object's lifecycle,userActionsand API operations, and the per-record verdict).VIEW_DEFAULT_TRANSLATIONSfor the provider-less path, byte-identical toen.ObjectGrid.tsx,RowActionMenu.tsx,useConsoleActionRuntime.tsxandActionRunner.tsare untouched. The grid branch's remount-to-refresh is untouched.Pins
packages/core/src/actions/__tests__/recordDelete.test.ts, 13 cases:errorkey);errorgoes to the runner);findOne;packages/app-shell/src/hooks/__tests__/useObjectActions.test.tsx, UNCHANGED: toast de-duplication and the ADR-0094 reset copy, including thefindOnefallback.packages/plugin-view/src/__tests__/ObjectView.gridDelete-10383.test.tsx, 9 cases through the realObjectGridagainst an in-memory source:delete, 0bulkDelete) and partial bulk;scripts/__tests__/check-i18n-call-site-keys.test.ts: the objectui#3845 pin follows the call site intorecordDelete.ts, with the same absent / present pair, plus one added sister assertion (deleteSuccessstill interpolateslabel).Reverse verification (
ablation-replace.mjs, WRAP mode, ate9668789f; expectations pre-registered)handleDelete/handleBulkDeleteback to their refresh-only bodiesTests 7 failed | 2 passed (9): the 5 delete cases and the 2 ADR-0094 cases red; the permission pair, which pinsObjectGrid, greena8eb765b== HEAD,git diff HEADemptyisPackageOwned→return false)Tests 6 failed | 22 passed (28)across core, console and plugin-view: every reset case red on all three, every env-owned control greenbc25ece0== HEAD,git diff HEADemptyGates (head
473c86933; code identical toe9668789f, which adds only changesets on top)473c86933: 43 check-runs, 40 success and 3 skipped, 0 failures. On the intermediate head6c374d382, shard 4/8 was red incheck-i18n-call-site-keys(the pin still pointed atuseObjectActions.ts).e9668789ffixed it by re-pointing the pin, without weakening it.turbo run build --filter='@object-ui/app-shell^...': 28/28. Type-check exits 0 for core, plugin-view and app-shell.vitest run packages/plugin-view/ packages/core/: 215 files, 3951 passed.packages/app-shell/in 6 shards: 775 files, 7624 passed, 9 skipped, 0 failed.scripts/__tests__/: 179 files, 5306 passed, 2 skipped by design. The 17 other text readers of the touched sources: 1111 passed.check:control-bytesOK.check:new-line-citations0 new.check-changeset-presence: 2 changesets over 3 released packages, and it demands no app-shell entry.check-changeset-no-majorOK.check:changeset-claimsexits 0 (the same 5 pending changesets, re-read, still true).check:i18n-keysOK. Governed guard: NOT GOVERNED.ObjectView.tsxequal to base;useObjectActions.tsno-explicit-any8 → 4; the new files have 0.@object-ui/core: minor(the new export) and@object-ui/plugin-view: minor(the behaviour change). ⛔ Nomajor.Acceptance notes
packages/core/README.mddoes not documentrecordDelete. None of the sibling action-module exports is documented there either, and the README export gate checks only README→code. Surfaced to the maintainer on the card; not added here.console.objectView.bulkDeleteConfirmkey. There is precedent on main (console.objectView.newin plugin-view, and nineconsole.objectView.viewType*keys in plugin-list). Reusing it is what keeps both paths word-for-word in every language.void recordDelete.run(...)in plugin-view has no.catch. A synchronous throw inside the bulk map would surface as an unhandled rejection, but no in-tree adapter throws synchronously (DataSource.deleteisPromise-returning everywhere). Left as is.onMutation, a delete refreshes twice (the handler bump plus the subscription). The console list behaves the same.check:changeset-claimslisted 5 pending changesets namingplugin-view/src/ObjectView.tsx(6726, 7070, 7499, 8653, 7779). None describes the delete handlers, and all are still true.DetailView.handleDeletecomment says theActionProvider'sonConfirm"will intercept" a nativewindow.confirm; nothing does.Session:
https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC(domain:ui seat 1). Body updated by the seat at473c86933after the contract review; the code is the dev's.Generated by Claude Code