Skip to content

Browse: Edit Row "Save Changes" can be stuck disabled with no indication why #1600

Description

@Devin-Holland

Summary

In the database browse view, opening a row in the Edit Row modal can leave the Save Changes button permanently un-clickable (disabled) while the JSON on screen looks fine. Closing the modal and re-opening it clears the condition. Reported by @Devin-Holland with a screenshot showing a valid-looking record (a versions array, cursor on line 20) and a greyed-out Save Changes next to an enabled Delete Row.

The user gets no indication of why Save is dead — no squiggle, no message, no toast, no tooltip.

Where

src/features/instance/databases/modals/EditTableRowModal.tsx

The button is gated on exactly two pieces of state:

disabled={!isValidJSON || isUpdateTableRecordsPending}

isValidJSON is written in only two places:

  1. Monaco's onChangesetIsValidJSON(isRecordJsonProbablyValid(updatedValue)) (line ~153)
  2. The render-time reset block (lines 76–82), which sets it back to truebut only when value changes, where value is derived purely from the fetched server record (searchByIdData?.data + syntheticAttributes).

value does not change while the user types. So once isValidJSON flips to false, the only things that can flip it back are another keystroke or the server record changing. Closing and re-opening the modal does neither: EditTableRowModal is rendered unconditionally by DatabaseTableView (only open toggles), so isValidJSON, updatedTableRecordData, and the Monaco buffer all survive a close/re-open of the same row.

Note also that isRecordJsonProbablyValid returns false for empty content, not just malformed content:

if (!content) { return false; }

Why there is no feedback (the regression)

af1fc3b (fix(browse): make record editors worker-free to end Monaco JSON OOM, #1370/#1499) moved both record editors to WORKER_FREE_JSON_LANGUAGE_ID and replaced the old marker-based gate with a main-thread parse.

Before that change, the gate was onValidate → Monaco JSON worker markers, so a disabled Save was always accompanied by a red squiggle on the offending line. The worker-free language has no validation markers at all — that is the point of it — so the gate lost its explanation but kept the disable.

Scope check: six submit buttons in the app gate on !isValidJSON. The three role modals (EditRoleModal, AddOrganizationRoleModal, EditOrganizationRoleModal) still use onValidate + the worker-backed JSON language, so they keep their squiggles and are not affected. The two affected ones are the record editors:

  • EditTableRowModal.tsx (reported)
  • AddTableRowModal.tsx — same pattern, same missing feedback (disabled={!addTableRecordData || !isValidJSON || isAddTableRecordsPending})

Reproduction candidates

Ranked, with the confidence I'd put on each. All are consistent with the code as read; none has been reproduced in a browser yet.

  1. Stale invalid buffer survives close/re-open of the same row. Type an edit that transiently breaks the JSON (e.g. delete a closing quote), then dismiss the modal without saving. React-query still holds the record under the same key, so data and value are unchanged on re-open → the reset block does not fire → the editor still holds the malformed buffer and isValidJSON is still false. Because the malformed part can be scrolled off-screen and draws no marker, the visible portion looks perfectly valid. Most likely.
  2. The buffer really is invalid somewhere off-screen. The screenshot shows lines 15–24 of a 24-line record; lines 1–14 are not visible. With no markers, a single stray character anywhere in the document disables Save silently.
  3. Empty buffer. Select-all + delete (or a paste that momentarily empties the model) sets isValidJSON = false via the if (!content) branch. Restoring content via a path that does not fire onChange would leave it stuck.
  4. isUpdateTableRecordsPending stuck true. Would also disable the button, but onRecordUpdate has no onError, and a rejected mutation still clears isPending, so this looks unlikely — and it would not be cleared by closing the modal, since it lives in the parent.

Hypothesis 4 aside, all of these share one root cause: a hard disable with zero feedback, over state that outlives the modal.

Suggested fix

A. Stop disabling Save on validity (recommended). The authoritative, caught parse already exists in the click handler and already toasts on failure:

const parsed = tryParseRecordJson(updatedTableRecordData);
if (!parsed.ok) {
    toast.error("This record isn't valid JSON — fix the syntax and try again.");
    return;
}

Reducing the gate to disabled={isUpdateTableRecordsPending} makes the button always clickable and turns every failure mode above into an explicit message instead of a dead control. It also removes the reason isValidJSON needs to be reset correctly at all. Same change applies to AddTableRowModal.

B. Give the error a location. SyntaxError from JSON.parse carries a position; surfacing "line N, column M" (in the toast, or as a manually-set Monaco marker) restores what the JSON worker used to provide, without bringing the worker back. Worth doing alongside A.

C. Reset the editor on open, not only on record change. Independent of A/B, re-opening a row should show the stored record, not the previous unsaved draft. Today an abandoned edit silently persists into the next open of the same row.

While in here, one adjacent latent bug in the same handler:

if (!updatedTableRecordData) {
    setIsModalOpen(false);
    return;
}

If the record refetches while the user has unsaved edits, the reset block clears updatedTableRecordData back to undefined. Clicking Save then just closes the modal — no save, no warning, edits gone. Worth a guard in the same pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    admin & dataAdmin Panel & Other Data Enhancements (Jira STUDIO-494)

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions