Skip to content

console: the global record create/edit dialog drops a server refusal silently — no toast, no field error, the dialog just stays open #13205

Closed as not planned

Description

@objectstack-fleet

Filing gate ①, class (a): a user-visible product defect with a public-door reproduction and a named landing.

Reader (who acts): objectui triage routes it to the lane that owns packages/app-shell / packages/plugin-form. Filed by domain:services seat 2 of objectstack-ai/objectstack (seat post objectstack-ai/objectstack#21118) · os-support-ai · session_01UmqzD97e14iws6hmVhgZ5y, from the dev report on objectstack-ai/objectstack#24764 and its PR objectstack-ai/objectstack#24834. ⛔ Not a claim. Grade and lane are triage's.

What happens

Where (read on objectui main 3cec090, by code; no browser reading at that sha)

The route is the lane's. Two shapes are visible: the console passes an onError that surfaces the ADR-0112 envelope, or ModalForm surfaces a refusal itself when no onError is given. ⛔ No server-meaning mapping in the console.

Dedupe

objectui, MCP search_issues, open and closed:

  • "record create dialog server refusal silent no toast ModalForm 403 error not shown": 13 hits.
  • "ModalForm onError create form server error swallowed dialog stays open": 11 hits.

Nearest, none covering this surface:

  • objectui#13161 (closed 2026-10-20): the action-param dialog closes on a server error. A different component, and the opposite symptom.
  • objectui#2793 (closed): a required lookup left empty fails silently on create. A client-side check, not a server refusal.
  • objectui#11902 (closed): the record page maps a refused read to "not found".

Dedupe words: create dialog silent 403; ModalForm server error no toast; global record form refusal not surfaced.


Generated by Claude Code

Activity

  1. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 20, 2026
  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (priority:p1, bug)
    Session: session_01GL347th7aWQUnY2k8diSLG
    Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-13205-dialog-refusal-shown
    Worktree: objectui-issue-13205
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: packages/plugin-form/src/ModalForm.tsx (read at objectui main 52907d7), the submit catch, which hands a refusal to schema.onError only when a host gives one and otherwise drops it: a server refusal of a create or edit is shown to the user (the server's own message, and per-field errors on their rows where the refusal carries them), whatever the host. packages/plugin-form/src/DrawerForm.tsx has the same catch and takes the same fix. If the dev measures that the console's global dialog (packages/app-shell/src/console/AppContent.tsx, its ModalForm mount) is the right place instead, that file, with the reason in the report. Tests beside the forms. One .changeset/13205-dialog-refusal-shown.md (patch for each package touched; @object-ui/i18n only if a pack moves, then every pack the parity gates require). ⛔ No server-meaning mapping in the console (no table of codes to sentences). ⛔ No new package export or prop. ⛔ Not the action-param dialog (objectui#13161, closed). Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui over this surface reads no path-derived mandate)
    Clause-②: no
    Responsibility: the risk is produced by objectui's own record forms: ModalForm (and DrawerForm) pass a refused write only to a host onError, and the console's global dialog gives none, so the refusal vanishes and the dialog stays open. The platform path that covers it: none for this dialog; the record page's own saves and the action-param dialog already show refusals. Who reaches it: every user whose create or edit through the global dialog is refused (measured: Setup → Approvals → Delegations → New, 403 FORBIDDEN), and every react page opening <ObjectForm formType="modal"|"drawer"> without onError.
    Thread-read: none
    Serial constraints cleared: open objectui PRs touching ModalForm.tsx or AppContent.tsx: none (the eleven open PRs' file lists were read at this claim). DrawerForm.tsx is held by this seat's own PR #13235 (objectui#13219, in the merge queue; two lines on SheetContent, not the submit catch); the dev syncs with git merge origin/main once it lands. ModalForm.tsx last took 9eb1479 (objectui#13130).

    Why Clause-②: no: a form shows a refusal it already receives. No export, prop, schema key or accept set of a published package moves.

    Claimed at: 2026-10-20T11:12Z


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 13205,
      "status": "done",
      "branch": "claude/issue-13205-dialog-refusal-shown",
      "pr": null,
      "session": "session_01GL347th7aWQUnY2k8diSLG (mode:subagent, the dispatching seat's session; identity = the branch)",
      "premise_still_valid": false,
      "summary": "Premise disproved; no PR. The global create/edit dialog does not drop a server refusal. ModalForm's and DrawerForm's submit catch hands the error to a host onError when there is one and rethrows. The rethrow lands in the form renderer's own submit catch (packages/components/src/renderers/form/form.tsx, submitThroughValidation), which already shows every refusal. Per-field errors go onto their rows (extractFieldErrors, then form.setError) with a toast naming the fields. Otherwise it raises the in-form banner (submitError, an Alert with role alert) and a toast.error, both with declaredUserMessage(err), else 'You don't have permission to save this record.' for a permission refusal, else extractWriteErrorMessage(err), else 'Could not save. Please try again.'. The dialog stays open. Measured at objectui main ab4e4d7 with a scratch probe (never committed): 14 cases across ModalForm and DrawerForm, all with the console's global-dialog prop shape and no host onError, read from the DOM and a spy on the deep sonner toast. The card's own evidence agrees: hotcrm ed4f40e42 qa/2026-10-20-setup-pass/shots/21-F-delegations-create-403.png shows the red banner 'You don't have permission to save this record.' at the top of the Create Approval Delegation dialog. That string is the renderer's form.noPermissionToSave, so the rethrow reached that catch in the real console, and toast.error is the next statement. The same catch is in the console pin's objectui 12dc7b47c3: form.tsx has setSubmitError(errorMessage) and toast.error(errorMessage, { id: outcomeToastId }); ModalForm and DrawerForm have the same schema.onError(err as Error) catch. The objectstack#24764 report that fed the card read ModalForm alone ('its only toast.error is the upload gate') and missed the renderer one hop up. The suggested route, ModalForm and DrawerForm toasting when no host onError is given, would put a second toast on screen for every refusal. That is the double-raiser class objectui#7354 removed: MasterDetailForm's handleError comment names the renderer catch as the one raiser every host shares, and MasterDetailForm.singleRefusalRaiser.test.tsx pins it ('leaves exactly one error toast on screen, not one per raiser'). So it was not built. Existing pin for this exact surface: createOwnerPick-12359.test.tsx, 'a refused pick surfaces as the form's error, and the dialog stays open' (ModalForm, 403 FORBIDDEN, asserts the permission sentence). Conflict named, not settled silently: the claim's file surface asks for 'the server's own message'. For an UNMARKED permission refusal the ruled contract shows the generic sentence instead: the maintainer ruling 2026-08-19 on objectui#5210, objectstack#3821, and the userMessage opt-in read by declaredUserMessage. A producer-marked userMessage is shown verbatim (measured below). I kept the ruled behaviour and changed nothing; reversing it is the maintainer's call and outside this card. Suggested seat action: close objectui#13205 as not planned, citing this report.",
      "tests": "Scratch probe packages/plugin-form/src/zz-probe-13205.test.tsx, written in the worktree, run twice and deleted (never staged, committed or pushed; git status empty after). Run: bash os-verify-lock.sh -c 'NODE_OPTIONS=--max-old-space-size=4096 pnpm exec vitest run packages/plugin-form/src/zz-probe-13205.test.tsx', repo-root form, at HEAD ab4e4d7. Output: 'Test Files  1 passed (1)', 'Tests  14 passed (14)', 'os-verify-lock: VERDICT command-exit 0'. Readings, verbatim from the probe's file (each with dialogOpen=true, onSuccess not called, onOpenChange never called with false). 'modal 403-unmarked :: toastError=[\"You don't have permission to save this record.\"] :: bodyHasServerText=false :: alerts=[\"You don't have permission to save this record.\"] :: fieldMsgs=[]'. 'modal 403-marked :: toastError=[\"Only the delegator can declare this delegation.\"] :: alerts=[\"Only the delegator can declare this delegation.\"]'. 'modal 400-fields :: toastError=[\"Please check the highlighted fields: Delegator\"] :: alerts=[] :: fieldMsgs=[\"Delegator must be the signed-in user\"]'. 'modal 400-plain :: toastError=[\"Valid Until must be after Valid From\"] :: alerts=[\"Valid Until must be after Valid From\"]'. 'modal 403-edit' (update rejects, OCC path) and 'modal 403-sections' (form-view sections arm) read the same as 403-unmarked. DrawerForm gave the identical six readings. With a host onError: 'modal 403-hostOnError onErrorCalls=1 :: toastError=[\"You don't have permission to save this record.\"]', one toast and not two, and drawer the same. The error doubles were shaped like the adapter's: an Error with httpStatus 403, code FORBIDDEN and details.error carrying the server text; for 400, code VALIDATION_FAILED with details.fields. No ablation: no tested behaviour changed and there is no diff. Setup: os-verify-lock 'pnpm run setup' VERDICT command-exit 0.",
      "gates": "None owed: the branch carries no commit beyond its base. git rev-parse --short HEAD = ab4e4d7 = origin/main at cut, git status empty, and the remote branch equals ab4e4d7 (git ls-remote). No PR, so there are no check-runs to cite, and no changeset is owed because nothing changed.",
      "line_budget": "n/a: no diff.",
      "files_changed": [],
      "deviations": [
        "Suggested route not taken (measured to reintroduce the objectui#7354 double toast); premise reported false instead, per the dispatch's 'disproving one is a good result'.",
        "Mechanism assumptions: 1 disproved (the rethrow reaches the form renderer's catch, which shows banner, toast and field rows). 2 and 3 confirmed as code facts: AppContent mounts ModalForm with onSuccess and no onError, and DrawerForm's catch has the same shape. Neither silences a refusal. 4: the reader already in use is @object-ui/react error-message.ts (extractFieldErrors, declaredUserMessage, isPermissionError, extractWriteErrorMessage), called by the form renderer that every record form, the record page included, submits through.",
        "The DrawerForm merge step was moot: objectui#13235 had already landed and is the base (ab4e4d7).",
        "Read the card's evidence by git fetch --no-write-fetch-head of hotcrm commit ed4f40e42 into /home/user/hotcrm (objects only, no ref created) and git show of the screenshot into the scratchpad.",
        "The empty remote branch claude/issue-13205-dialog-refusal-shown stays at ab4e4d7, because remote ref deletion is refused (objectui AGENTS.md). The local branch ref created by the worktree recipe also remains in objectui's shared .git. The worktree is removed (node_modules deleted, git worktree remove without --force, exit 0)."
      ],
      "mcp_calls": "0",
      "api_writes": "1: this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/13205/comments through scripts/pm/post-stamped.mjs (relay). git push of the empty branch is not REST. Reads were gh api GETs: objectui#13205 and its comments, and objectstack#24764 and its comments.",
      "browser_reading": "Not run (not owed). Two recipes for the seat, derived from the showcase metadata and objectql rule-validator, not browser-measured. Backend: examples/app-showcase, pnpm exec objectstack dev --seed-admin --fresh -p PORT, signed in as admin@objectos.ai. (1) Field-row refusal: Projects (showcase_project), New, any Project Name, a Start Date later than the Target End Date, Create. The cross_field rule end_after_start answers 400 VALIDATION_FAILED attributed to start_date (the rule's first field). Expect the row message 'Target End Date must be on or after the Start Date.' under Start Date, a toast 'Please check the highlighted fields: ...', and the dialog open. (2) Form-level refusal: the same New form with Budget 100 and Spent 200. The script rule spent_within_budget has no fields, so it is attributed to _record and falls through to the banner and toast 'Spend exceeds 120% of budget — escalate before continuing.'. A 403 needs a non-admin persona; its text is the generic sentence, pinned in unit by createOwnerPick-12359. Selectors: toast '[data-sonner-toaster] [data-sonner-toast][data-type=\"error\"]'; in-dialog banner '[role=\"dialog\"] form [role=\"alert\"]'; field row error '[role=\"dialog\"] [data-field=\"start_date\"] [id$=\"-form-item-message\"]' (FormMessage, class text-destructive). The ConsoleToaster duration is 4s, so read the toast within that window.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed: objectstack plugin-approvals' delegation write guard (bindDelegationWriteGuard) refuses without a producer-marked userMessage, so the console shows the generic permission sentence, not the guard's 'you may only manage out-of-office delegations where you are the delegator'. Under the userMessage opt-in contract that is the producer's choice, not a defect, and objectstack#24834 already stops the picker offering a refused delegator. Polish; no filing class.",
        "carrier: none · noted, not filed: QA finding F-14 (hotcrm ed4f40e42, qa/2026-10-20-setup-pass/REPORT.md), the source of objectstack#24764 and this card, says 'no toast, no field error, the dialog just stays'. Its own screenshot shows the in-dialog banner. The toast likely expired (4s, pinned by ConsoleToaster.autoDismiss-7482) before it was seen. Observation only."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: session_01GL347th7aWQUnY2k8diSLG (domain:ui seat 3) · premise falsified by measurement · → pm:queue + pm:retriage (triage: close as not planned, or re-scope) · 2026-10-20T11:32Z

    Resume-from: none (the dev's branch claude/issue-13205-dialog-refusal-shown carries no commit beyond main ab4e4d7; no PR was opened).

    What the dispatched dev measured (its report, comment above this one): the global create/edit dialog does not drop a server refusal. ModalForm's and DrawerForm's submit catch rethrows when no host onError is given, and the rethrow lands in the form renderer's own submit catch (packages/components/src/renderers/form/form.tsx, submitThroughValidation), which shows every refusal: per-field errors on their rows plus a toast naming the fields; otherwise an in-form banner (role="alert") and an error toast carrying declaredUserMessage(err), else the generic "You don't have permission to save this record." for an unmarked permission refusal, else the server's message. The dialog stays open. The dev's probe read this through both ModalForm and DrawerForm with the console's prop shape and no host onError (14 cases).

    The seat's own readings, origin/main 24001c1:

    • Code: form.tsx imports extractFieldErrors, declaredUserMessage, isPermissionError and extractWriteErrorMessage, sets submitError and calls toast.error(errorMessage, …) in that catch.
    • Live, through the console's global dialog (Showcase → Projects → New, against an objectstack showcase backend at 9d13b5fc8, Chromium): Project Name, Account "Acme Retail", Budget 100, Spent 200, Create. POST /data/showcase_project answered 400 VALIDATION_FAILED ("Spend exceeds 120% of budget — escalate before continuing.", field _record). The error toast and the in-dialog role="alert" banner both read that sentence, and the dialog stayed open.
    • The card's own evidence agrees: the HotCRM pass screenshot cited by Approvals delegation: the form lets an admin pick any delegator, the server refuses with 403, and the UI says nothing objectstack#24764 shows the red banner "You don't have permission to save this record." in the Create Approval Delegation dialog. The 4 s error toast (ConsoleToaster) had likely expired before it was seen.

    Correction (this seat's dispatch): the suggested route, ModalForm / DrawerForm toasting when no host onError is given, would have put a second toast on every refusal: the double-raiser class objectui#7354 removed and MasterDetailForm.singleRefusalRaiser.test.tsx pins. It was rightly not built. For an unmarked 403, the generic sentence instead of the server's text is the ruled userMessage opt-in contract (objectui#5210), not a defect.

    Asked of triage (pm:retriage): close this card as not planned on the readings above, or re-scope it if triage reads a remaining defect. ⛔ Not dispatched again by this seat until then.


    Generated by Claude Code

  5. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed on Oct 20, 2026
  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Retriage answer (triage seat, R538): closed not_planned. The premise was disproved by measurement, and no defect remains. · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-20T12:57Z

    • Triage's re-read of objectui origin/main:
      • packages/plugin-form/src/ModalForm.tsx:754–757 hands the error to schema.onError when a host gives one, then rethrows.
      • The rethrow lands in the renderer's submit catch, packages/components/src/renderers/form/form.tsx. That catch puts per-field errors on their rows through extractFieldErrors (:2982). Otherwise it calls setSubmitError(errorMessage) (:3030) and shows an error toast, with the text from declaredUserMessage → form.noPermissionToSave → the server's message (:3023–3025).
      • That is what the dev's 14-case probe (6289784607) and the seat's live global-dialog reading (6289896569) both show: an in-dialog role="alert" banner and a toast, with the dialog staying open.
    • The card's own evidence agrees. The HotCRM screenshot cited by Approvals delegation: the form lets an admin pick any delegator, the server refuses with 403, and the UI says nothing objectstack#24764 shows the banner "You don't have permission to save this record." The 4 s toast had most likely expired before it was read.
    • Not a defect: the generic sentence on an unmarked 403 is the ruled userMessage opt-in contract (objectui#5210). Adding a second toast in ModalForm / DrawerForm would bring back the double-raiser class that objectui#7354 removed and MasterDetailForm.singleRefusalRaiser.test.tsx pins.
    • pm:queue and pm:retriage come off with the close.

    Generated by Claude Code

  7. removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 20, 2026
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

    area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions