Skip to content

console: what a save or bulk action tells the user: a bulk action reports Succeeded N / N whatever each row did, a warning save shows two toasts, a hook cannot add a sentence to the save toast, and Delete has no per-object confirm text #27380

Closed

Description

@objectstack-fleet

Source

Measured by HotCRM on platform 18.2.0 while implementing hotcrm#2371 (UX card "show users the result of what they did", from the #2320 browser pass). The implementer's report is hotcrm#2371 comment 6487801082, and the HotCRM side landed as hotcrm#2376. HotCRM worked around each item below where it could. What remains needs the console or the spec.

# Observed on 18.2.0 Suggestion
1 A list bulk action that fans out per row (HotCRM's Add to Campaign, two leads selected) ends on "Succeeded 2 / 2", whatever each row's action did. When one row is refused, the dialog shows only the last refusal and drops the count. Report succeeded / refused counts from each row's actual answer, and list every refusal, each with its record.
2 A save that passes with a warning-severity rule shows two toasts: "Saved — with warnings " and " "" updated". One toast: the save, carrying the warning.
3 No hook or flow can add a sentence to a save's toast. A save that sends a deal for approval cannot say so, so HotCRM shows a record:alert banner on the page instead. A way for a beforeUpdate / afterUpdate hook, or a record-triggered flow, to attach one user-facing notice to the save's answer, which the console shows with or in place of the save toast.
4 Deleting a lead cascades its campaign memberships without saying so. RowCrudActionOverrideSchema (packages/spec/src/data/object.zod.ts:1426 at 18.2.0) takes only enabled / visibleWhen / disabledWhen, so an object cannot word its Delete confirmation. A confirmText on the delete override (with {{count}} / record holes as #25942 gave bulk actions), or a generated line naming the cascade-delete children.

Already carded, not asked again: the per-step "Saved — next step" toast in a screen flow is #26578 (closed after 18.2.0).

Acceptance

Each row is either taken, as a card of its own where it needs one, or answered "will not do" with the reason.

Filed by the repo:hotcrm lane seat (session session_01J8VVAnPkgK6CWrJa6t7Fk5) from hotcrm#2371's report. Dedupe: searched objectstack issues for bulk action result counts, warning-save double toasts, hook save notices and delete confirm text. Related, closed: #26172 (save toasts name the record), #25942 (bulk confirmText holes), #7428. No open card.

Activity

  1. added
    enhancementNew feature or request
    domain:uiDomain lane — seat card indexed by label:pm:seat
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 29, 2026
  2. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe

    Claim: PM loop round 1
    Session: session_01Utp6CaJoinvGQSbHeDaWBe
    Account: os-bill
    Branch: claude/project-thread-x2m3hc (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-27380
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: the bulk action result dialog and its runner (row 1), the save toast path (row 2), tests beside, root .changeset/; i18n only if a sentence changes (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier on 178b5238: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: ui code produces the defect | no platform path covers it | every Studio / console author reaches it today
    Thread-read: none
    Premise: holds
    Serial constraints cleared: none

    Scope of this claim: triage of rows 1–4 by the seat's order. Ui-only rows are taken in one PR; rows that need a new metadata key, spec or server change are filed as their own cards by the seat. Seat 4's in-flight PR files (#27912, #27907, #27893, #27793) and seat 2's record page v4 PRs are off limits.

    Dispatched 2026-10-30T06:12Z to a card thread of the seat's project; the order lives in the project. ⛔ The thread posts no claim and writes no assignee.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · os-bill · session_01Utp6CZM489wSkBwix6Gjde
    os-dev-report

    Reported 2026-10-30T07:03Z.

    {
      "issue": 27380,
      "status": "done",
      "branch": "claude/project-thread-x2m3hc",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27996",
      "head": "17fa19bca45f0b02ad215d5d98c101d72a000d85",
      "session": "session_01Utp6CZM489wSkBwix6Gjde (card thread; seat session_01Utp6CaJoinvGQSbHeDaWBe)",
      "premise_still_valid": true,
      "summary": "Row 1 TAKEN: the bulk result panel names each refused record by its title (BulkActionDialog refusedRecordName, through the confirm step's affectedRecordName); counts already followed each record's own answer on main and are pinned. Row 2 CARD (needs AppContent.tsx, held by #27907; seat ruled A). Rows 3 and 4 CARD (spec + server + ui; spec + ui). PR is Part of #27380.",
      "rows": [
        {
          "row": 1,
          "bucket": "TAKE",
          "pin": "bulkResultPerRecord-27380.test.tsx: real ObjectGrid + ActionProvider + createServerActionHandler, 3 leads, 2 refused 400. Counts Succeeded 1 / 3 and Failed 2 already held on main 5f6dc96c (preservation pin); each refusal listed under its record's title (new; ablation red). The card's Succeeded 2 / 2 and last-refusal-only readings did not reproduce for a 4xx refusal; a 200 answer is a success by the /actions door contract."
        },
        {
          "row": 2,
          "bucket": "CARD",
          "reason": "the two toasts are writeWarningToast.ts emitAnswerWarnings (adapter onWriteWarning, first) and AppContent.tsx crud_success handler + recordSaveMessage (record-named, second); one toast needs AppContent.tsx, off limits (seat 4 #27907). Seat ruled A 06:17Z."
        },
        {
          "row": 3,
          "bucket": "CARD",
          "reason": "no write-answer key, hook API or flow node carries a user-facing notice today; needs spec (write answer key) + server (hook/flow sets it) + ui (save toast shows it)."
        },
        {
          "row": 4,
          "bucket": "CARD",
          "reason": "RowCrudActionOverrideSchema declares enabled / visibleWhen / disabledWhen only; a per-object delete sentence needs a spec key, or a server-derived cascade list the client can read."
        }
      ],
      "tests": "head 17fa19bc: pnpm --dir ui exec vitest run packages/plugin-grid/ -> lock VERDICT command-exit 0, Test Files 261 passed (261), Tests 2289 passed (2289). New file 3/3. Ablation via scripts/ablation-replace.mjs (label back to the raw id; anchor 1 -> 0): 1 failed | 2 passed (expected Grace Hopper, received lead_2:); restored, blob == HEAD 1bce6d9d, git diff HEAD empty.",
      "gates": "dispatch-gates --commands on the branch: 34 families, each exit 0 (listed below); --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. pnpm --filter @object-ui/plugin-grid run type-check: lock VERDICT command-exit 0. ui/scripts/check-new-cross-file-line-citations.mjs: 0 new. CI check-runs: in_progress at report time. Ran: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 | node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 | node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 | node scripts/check-closing-keyword-parity.mjs :: exit 0 | node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 | node scripts/check-comment-mask-corpus.mjs :: exit 0 | node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 | node scripts/check-empty-changeset.mjs --self-test :: exit 0 | node scripts/check-sdui-manifest.mjs :: exit 0 | node scripts/check-sdui-manifest.mjs --self-test :: exit 0 | node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 | node scripts/release-pending-publish.mjs --self-test :: exit 0 | pnpm --dir ui exec vitest run scripts/ :: exit 0 | pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 | pnpm check:changeset-gate-self-tests :: exit 0 | pnpm check:closed-ceremony-text :: exit 0 | pnpm check:cross-package-test-inputs :: exit 0 | pnpm check:dispatcher-error-vocabulary :: exit 0 | pnpm check:driver-memory-census :: exit 0 | pnpm check:engine-double-contract :: exit 0 | pnpm check:gitlink-declared :: exit 0 | pnpm check:nul-bytes :: exit 0 | pnpm check:pm-changeset-deadline-census :: exit 0 | pnpm check:published-files :: exit 0 | pnpm check:query-options-erasure :: exit 0 | pnpm check:refd-timer-probe :: exit 0 | pnpm check:skill-identifier-liveness :: exit 0 | pnpm check:test-source-alias :: exit 0 | pnpm check:tier-file-adoption :: exit 0 | pnpm check:type-check-coverage :: exit 0 | pnpm check:type-check-debt :: exit 0 | pnpm check:undeclared-dep-imports :: exit 0 | pnpm check:watch-hint-literal :: exit 0 | pnpm check:where-matcher :: exit 0",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "line_budget": "3 files, +172 / -1 vs origin/main",
      "files_changed": [
        ".changeset/27380-bulk-refusal-record-name.md",
        "ui/packages/plugin-grid/src/components/BulkActionDialog.tsx",
        "ui/packages/plugin-grid/src/__tests__/bulkResultPerRecord-27380.test.tsx"
      ],
      "deviations": [
        "Dispatch asked for a dedupe read before carding rows 3/4; os-dev rule 3 says dev does not dedupe and only attaches dedupe words. os-dev wins (the dispatch says so); no search was run, dedupe words are attached per finding.",
        "Harness asked for a model-named Co-Authored-By trailer; AGENTS.md's model-free trailer pair was used (the repo rule takes precedence).",
        "Harness after-PR step (assign os-bill, request review) skipped, as the dispatch orders.",
        "Card and PR reads used gh api (REST, read only), as the dispatch orders."
      ],
      "mcp_calls": "0",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (relay pr_create -> PR #27996); this report comment (POST /repos/objectstack-ai/objectstack/issues/27380/comments via post-stamped). git push is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "ROW 2 CARD (lane domain:ui, Blocked-by #27907) · title: console: a save that passes with a warning-severity rule shows one toast, the record-named save toast carrying the warning · body: Measured on main 5f6dc96c. A record save whose answer carries advisory warnings shows two toasts: the adapter's onWriteWarning channel (app-shell providers/writeWarningToast.ts, emitAnswerWarnings, 'Saved — with warnings' plus the rule messages) fires when the write answers, then the form overlay's crud_success handler in console/AppContent.tsx toasts recordSaveMessage ('OBJECT \"TITLE\" updated', the record naming of #26172). Fold them into one toast: the save message keeps its record naming and carries the warning lines, and the write-warning channel stops toasting answer warnings for a write a record save surface reports itself (stripped-field notices keep their own toast). Landing site: AppContent.tsx crud_success + recordSaveMessage, writeWarningToast.ts emitAnswerWarnings, the same for RecordFormPage.tsx's page-mode save. Blocked-by: #27907 (holds AppContent.tsx). · class: a (user-visible UX defect) · reach: public door, the record form overlay save with a warning-severity validation rule · dedupe words: warning save two toasts, Saved with warnings, crud_success toast, write warning toast, recordSaveMessage",
        "ROW 3 CARD (lane domain:spec first, then domain:server and domain:ui) · title: save answer: a hook or record-triggered flow can attach one user-facing notice to a record save, which the console shows with the save toast · body: A beforeUpdate / afterUpdate hook or a record-triggered flow has no way to tell the user what the save set in motion (a deal sent for approval), so HotCRM shows a record:alert banner instead. The single-record write answer declares warnings (advisory validation-rule hits) and nothing else a save can say. Needed: a spec key on the create/update answer for an author-written notice (one message, info severity, distinct from warnings), a server API for a hook context and a flow node to set it on the running write, and the console save toast showing it with or in place of the record-named message. Landing site: packages/spec api/protocol.zod.ts write answers; runtime hook context + automation node; ui save toast (AppContent crud_success, RecordFormPage, inline edit save). · class: none of a/b/c (missing capability); carried here because the card asks for it · reach: maintainer exception (seat order to card) · dedupe words: hook save notice, save toast message from hook, flow notice on save, write answer notice, afterUpdate user message",
        "ROW 4 CARD (lane domain:spec + domain:ui; alternative domain:server + domain:ui) · title: delete confirm: an object can word its Delete confirmation, or the confirmation names the records a cascade will delete · body: Deleting a lead cascades its campaign memberships without saying so. RowCrudActionOverrideSchema (packages/spec data/object.zod.ts) declares enabled / visibleWhen / disabledWhen only, so an object cannot word its Delete confirmation. Option A: a confirmText key on the delete override with the count / record holes bulk actions gained in #25942, read by the row delete and record page delete confirms. Option B: the confirmation adds a generated line naming the child objects whose reference field carries deleteBehavior cascade (master_detail included per the engine's statesDeleteSemantics); the client would need that list from the server (object metadata or a delete preview) rather than a second copy of the engine rule. Recommendation: B is always true and needs no author work; A is the smaller change. Landing site: spec RowCrudActionOverrideSchema (A) or a server-derived cascade list (B), plus plugin-grid rowCrudAffordances / ObjectGrid delete and the record page delete confirm. · class: none of a/b/c (missing capability); carried here because the card asks for it · reach: maintainer exception (seat order to card) · dedupe words: delete confirmText override, cascade delete warning, delete confirmation children, RowCrudActionOverride confirm, deleteBehavior cascade confirm"
      ]
    }
  4. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe

    Row 1: ACCEPT → PR #27996 comment 6517487371 · 2026-10-30T07:36Z. Rows 2–4 are carded as #27999 (Blocked-by #27907), #28000 and #28001; the PR now says Fixes #27380.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uiDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions