Skip to content

Make the form pickers keep typed text, start with large types and survive a lost circuit - #2655

Merged
pournasserian merged 9 commits into
v2from
2639-picker-fixes
Oct 20, 2026
Merged

pournasserian merged 9 commits into
v2from
2639-picker-fixes

Conversation

@pournasserian

@pournasserian pournasserian commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Closes #2639

What

Follow-ups from the independent review of #2622 (F-FRM, #2426).

  • The search box owns its text. The server never writes value back, so a key typed while a search runs is never lost. admin.js empties the box after a choice.

  • The island takes no option list. Its parameters are the field, the chosen id and text, so the circuit's start message stays small however many entries or assets there are.

    • Before the circuit attaches, the prerendered select loads its own choices: the 200 newest, and the chosen one named.
    • EntryEditor no longer loads every entry and asset for its pickers.
  • Every read goes through a scope of its own. The pickers of a form load at once, and searches overlap as the person types, while a DbContext takes one query at a time. This is the rule from Run the header counts in their own scope so signed-in pages load on PostgreSQL #2642.

  • A search waits for a 250 ms pause in typing, and only the newest search shows its results.

  • The media picker asks the library for its text. MediaQuery gains:

    • Text, which matches a file name or folder whatever its case;
    • Newest, which returns that many of the newest assets, newest first.

    The text is matched in .NET over the names alone, because SQLite's lower() folds ASCII letters only. Only the matching assets are read after that, so the picker no longer reads every asset on every keystroke.

  • Enter in the box never submits the form. It chooses the only match, or enters the list when there are more (admin.js).

    • The list says which text it answers (data-for). Enter acts only when the box still holds that text, so a fast typist never chooses a match left from an earlier word.
    • Text, not a count of keys, so keys typed while the connection was down can't put the box and the server out of step.
  • Focus stays in the box after a choice, Clear or Escape.

  • The choice is kept with the circuit ([PersistentState], keyed by field). A circuit paused and resumed, as after a laptop sleeps, keeps what the person chose rather than going back to the stored value. A new stored value replaces it, as when enhanced navigation shows another version of the entry.

  • A choice is named by the type's first text field, in definition order, with its state and short id. An entity's fact value counts. The chosen option in the prerendered select is named the same way.

  • A lost circuit doesn't reload a page whose form holds a picker or any typed input, such as a rejection reason on the set view.

    • The reconnect display says the connection was lost and the page keeps what was typed. It offers to reload, and says that reloading loses it.
    • The picker boxes are disabled. The hidden choice still posts.
    • Enhanced navigation is turned off for the rest of the page's life, so every link loads its page in full. So do Back and Forward: Blazor handles them without looking at that attribute, so a capture-phase popstate listener reloads the page instead. Without a circuit nothing can render the next page's islands, and an enhanced navigation would keep this page's: another version's picker choice, or another set's items.

A choice made in the select before the circuit attaches is still replaced by the stored one. That is #2654. The select still reads every entry of its type to show the newest 200. That is #2656.

Why

  • No value on the box rather than @bind. Every server render writes a bound value back, and that write is what dropped keys under latency. The box needs no server state between keys: the search reads the text from the event.
  • Full page loads after a lost circuit belong to this PR. Before it, a lost circuit always reloaded the page. Make the form pickers keep typed text, start with large types and survive a lost circuit #2639's acceptance keeps the page, so the form's input survives. A page kept without a circuit can't render the next page's islands, and an enhanced navigation would carry this page's dead ones. Turning enhanced navigation off once the page is kept completes that change.
  • The select loads its choices in prerender. Anything passed as an island parameter travels in the circuit's start message, and SignalR closes the connection when that message passes 32 KB.

Tests

  • Browser, on PostgreSQL when it is configured (as in CI), otherwise SQLite:
    • A keyboard-only path from the top of the page, with no FocusAsync, on a target type of 500 entries:
      • type at full speed and keep every character;
      • Enter chooses the only match without submitting, and focus stays in the box;
      • Tab to the rationale and save.
    • Searches with no pause overlap, the newest one shows, and the circuit stays up.
    • Enter never chooses a match left from an earlier search.
    • A paused and resumed circuit keeps the choice, and Enter still chooses after keys typed while it was paused. Without the fix, the test fails.
    • Starting from the published version gives the picker that version's value, and the save proposes it.
    • A rejected circuit keeps the form, the typed title and the choice, and disables the search. The form then saves with both.
    • A rejected circuit keeps a set view's typed rejection reason, and N then loads the next set in full with its own items.
    • With the circuit lost, "Start from the published version" loads the published value into the picker.
    • Back after an enhanced move, with the circuit lost, loads the earlier page in full: the draft with its own choice, or the first set with its own items.
    • Without the fix, both of these fail.
    • The axe check on a form with pickers is the kitchen-sink form from Keep the entry form's base version and the values a person didn't touch #2638.
  • Integration, on SQLite and PostgreSQL: media text search finds "Ärzte.png" by "ärzte" and "ÄRZTE", and Newest keeps the newest first.
  • bUnit:
    • The island's parameters are only strings and flags.
    • Only the newest of two overlapping searches shows.
    • A media search asks the library for its text and the 10 newest.
    • An entry is named by its type's first text field, and the select names the chosen entry.

dotnet build -warnaserror, the unit and bUnit suite, the browser tests on SQLite and PostgreSQL, and the form and media tests pass locally. No new package and no new project reference.

Checklist

  • Linked to an issue a maintainer has confirmed
  • Targets v2
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

@pournasserian pournasserian added this to the M1 Governance core milestone Oct 20, 2026
@pournasserian pournasserian added v2 FluentCMS v2 (v2 branch) type:bug Defect area:forms labels Oct 20, 2026
@pournasserian
pournasserian merged commit a245fa2 into v2 Oct 20, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2639-picker-fixes branch October 20, 2026 02:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:bug Defect v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the form pickers keep typed text, start with large types and survive a lost circuit

1 participant