Repository navigation
Add search-backed pickers for reference and media fields - #2622
Merged
Merged
Conversation
This was referenced Oct 19, 2026
pournasserian
added a commit
that referenced
this pull request
Oct 19, 2026
…ch (#2638) Closes #2630 Closes #2633 ## What Follow-ups from the independent reviews of #2613 and #2619 (F-FRM, #2426). The generated form now keeps the version it was made from and the values the person didn't touch. - **Base version.** The hidden `BaseVersion` comes from the form being shown. A form refused for its own problems keeps the version it started from, so its next save is refused as a conflict instead of overwriting a change saved meanwhile. - **Untouched values.** - `FieldValues.Read` and `FactValues.Read` take the stored value. - A posted text that shows the same as the stored value keeps the stored JSON. Seconds, offsets, `4.50` and LF line breaks survive a save. - Textarea line breaks are compared as LF. Only CR and CRLF are converted; other line separators stay as typed. - A stored value the field's current type no longer takes, as after a number field became text, is read from what was posted. - A `text` value with line breaks is shown in a textarea, since a single-line input drops them. - A textarea value that starts with a line break gets one more, since HTML drops the first. - **Which draft the form shows.** - When the latest version isn't the published one and no set is reviewing it, a note names the version shown. It offers `?from=published`. The update is still made from the latest version, so the version check holds. - A reverted create, which leaves nothing published, says so too. - **Entity facts.** - The owner list holds every user, and disabled users are labelled "(disabled)", so a stored owner stays selected. - Owner ids are compared as GUIDs, and the stored text is kept. - A posted owner that is neither a user nor the fact's stored owner is refused. - **Untouched facts are kept exactly.** A fact whose value and parts all show as stored is saved as stored, with its property order, spaces and any other properties. - **A stored plain value** of a type made an entity type later shows as the value of a fact with no details yet, flagged unsourced. It no longer fails the save or is dropped. A fact is an object with a `value`, so a plain JSON object is a plain value too. - **An owner who is no user**, such as one an agent set, shows as "Unknown user" on its own field and stays selected. - **The published version the values came from** is posted under `?from=published`, and whenever the latest version is the published one. - `EntryEdit.PublishedBase` carries it to `EntryEdits`. That checks it before the unit of work, and again against the version the operation is based on. - If publishing or a revert has moved on, the save is refused as a conflict, so it can't undo what was published. - **Booleans.** An unchecked box saves `false` only when a value was stored, the field is required, or its fact details were given. Otherwise the field is left out, as an empty field is. A name-only edit no longer adds an unsourced `{"value": false}` fact. - **Fact details for an empty value** are reported next to the field instead of being dropped. - **`EntryEdits`** checks the rationale and sources before it writes. - **`PickerField`** names `aria-controls` only while its list exists. The new axe check found it pointing at a missing id (#2622). ## Why - **Keep the stored JSON when the text matches.** No widget can show every stored value exactly, such as a date-time's seconds or a number's trailing zero. Comparing what the widget would show tells an untouched field from an edited one without client script. - **The base version stays the latest version under `?from=published`.** Another base would weaken the version check. The person sees which values they start from, and the set's diff shows the full change. ## Tests - Unit: - `FieldValues.Read` keeps stored date-times, numbers, CRLF text and JSON whose text matches, and replaces a changed one. - Unchecked booleans. - `FactValues.Read` keeps a stored owner's case and a stored time's seconds. - An unknown owner is refused. - `HasDetails`. - A JSON object without a `value` is a plain value. - A U+2028 in an edited text is kept. - A picker without matches has no `aria-controls`. - Integration (`EntryFormScreenTests`, `EntityFormScreenTests`): - The #2630 overwrite scenario ends in the conflict message. - After a published revert, the form names the reverted draft and offers the published version. A form from a published version since replaced is refused. - A reverted create says so. - Editing an entry written before its type held facts shows its plain values, a JSON object included, and saves its facts. - A title-only edit keeps several values exactly as stored: - a CRLF body that starts with a line break; - a multi-line `text` value, which gets a textarea; - the date-time text `09:30:45.5+02:00`; - `4.50`; - an absent boolean. - `EntryEdits` refuses a blank source, and an update whose published base isn't the published version. Neither writes a version or an event. - A form open on the published version when a revert publishes an older one is refused on save. - An untouched value after its field changed from number to text is saved as text. - Editing a published entry proposes an update from version 1, and a stray draft is named. The saved set is submitted from the set view. - The unknown enum value is reported. - A name-only entity edit keeps the other fact byte for byte, including a disabled upper-case owner, a source in another order with a trailing space, and times with seconds and offsets. It also keeps an owner who is no user, and an absent boolean. - Problems and the unsourced marker sit inside their field's fact fieldset. - A forged owner and fact details without a value are refused. - Browser: - A title-only edit in Chromium keeps a multi-line text and a rich text that starts with a line break. - The axe check covers a form with every widget, and a refused submit. The keyboard path through a form and its save is `FormPickerTests` from #2622. `dotnet build -warnaserror`, the unit and bUnit suite, the browser tests and the integration suite pass locally. No new package and no new project reference. ## Checklist - [x] Linked to an issue a maintainer has confirmed - [x] Targets `v2` - [x] Tests or docs updated where behaviour changed - [x] No tool-generated co-author trailers or footers
Merged
4 tasks done
pournasserian
added a commit
that referenced
this pull request
Oct 20, 2026
…vive a lost circuit (#2655) 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 #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. #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 #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 - [x] Linked to an issue a maintainer has confirmed - [x] Targets `v2` - [x] Tests or docs updated where behaviour changed - [x] No tool-generated co-author trailers or footers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2621
What
The third slice of F-FRM (#2426): pickers for reference and media fields (FRM-01), backed by search (
ISearch, #2566, #2606) and the media library (#2543).PickerField(Components/Forms) is an interactive island inside the static generated form.FieldWidgetrenders it for reference and media fields.Form.Values[field], so the form posts and saves exactly as before.listboxof buttons that rove with the module from Add keyboard-first review with roving focus, browser tests and WCAG checks #2585. ArrowDown moves from the search box into the list, the arrows move through it, Enter chooses and Escape clears the matches.admin.js: adata-ui="rove-input"box moves focus to the first item of the list itsaria-controlsnames on ArrowDown.Why
FieldOption[]rather than an interface. An island's parameters cross into the circuit as JSON. WithIReadOnlyList<FieldOption>, the browser refused the root components with "The list of component operations is not valid". The parameter says why in a comment.SearchQuery.Fieldsexists for it once a field's settings name a filter), list-of-reference widgets, and rule hints (FRM-03, needs F-RUL F-RUL: Rule book as versioned content #2419).Tests
PickerFieldTests):GeneratedFormTestsrenders the reference and media widgets before the circuit, as selects.FormPickerTests), keyboard alone: type the title, Tab to the picker, type part of a product's name, press ArrowDown and Enter, then write the rationale and save. The stored entry references the product.dotnet build -warnaserror, the unit and bUnit suite, the browser tests and the SQLite integration suite pass locally. The local PostgreSQL run on a loaded machine had nine failures in untouched classes, the same pattern as the last three PRs. CI runs the full PostgreSQL suite. No new package and no new project reference.Checklist
v2