A settings link to a singleton controller only worked on a plain left
click; a new tab landed on the list route, which answers 404, and every
non-singleton link paid an extra /singleton request. The settings index
now links like any controller link, and the list view opens the
controller's one record when its list answers 404 and the singleton
route answers an id.
A move made while a save runs is queued and sent after it settles, so
out-of-order completion can no longer leave the database on an older tree
than the screen. A failure that a waiting save supersedes keeps the later
moves, and a response tied to an earlier load or partition leaves the
current tree and its last saved state alone.
- the reorder route renders ReorderView with a ReorderBranch per level
- ReorderBranch renders nodes from the REORDER_PAGE context and moves
through it
- displayHtml uses a dedicated DOMPurify 3.4.16 instance: EDITOR_TAGS, href
only on <a> through isAllowedLink, DROPPED elements go with their content
- string in, string out: parsed into DOMPurify's inert document, nothing
adopted into the live document before binding
- fails closed (empty string) unless isSupported and a fixed canary hold
- Vitest browser mode in Playwright Chromium (npm run test:browser) covers
the clobbering vectors, off-origin form, iframe, meta refresh, links and
the inert parse; happy-dom keeps engine-independent assertions
- signpost: activation refusals, host overrides, D-02 pattern layers
incl. PatternFrom, ReferenceOptions depths
- seaglass: removal report, bluemonday bypass corpus, Check
- pact Option.Depth omitempty and the menu item type contracts; party
activates menu item types after Register and before the first Boot
- cabana: repeater compile and decode, dependsOn compile and recheck,
richeditor keys and notices, reorder and singleton boot checks
(Docker-free); repeater save, dependent recheck, richeditor save,
reorder and singleton routes on Postgres; conformance cases for the
documented 404, 409 and 422 answers of the new routes
- admin: richeditor value contract, link popover, notices and chunk
failure; repeater bounds, remove and reorder, collapsed rows;
dependent dropdown states and last request wins; reorder
announcements, partition switch and tree rules; SingletonForm spec
- gate: required names in the go, admin and postgres stages
- ReorderView: indented tree with drag handle, move up/down, outdent and
indent (buttons and arrow keys on the handle), pointer drag before, after
or inside a row and never into its own subtree; one POST with the whole
node set 400 ms after the last move, flushed on leave; a failed save
restores the saved tree, a 409 reloads; loading, empty, load-failure and
403 states; partition selector
- ListView: outline Reorder button before New record, passing the filtered
partition
- SettingsIndexView opens a singleton controller's record through
GET …/singleton; FormView in singleton mode has no Delete and returns to
the settings index
- reorder.* and messages.list.reorder phrase keys (en, pl), smoke spec, docs
- RepeaterField: rows with drag handle, index, titleFrom title, error chip,
move up/down (buttons, ArrowUp/ArrowDown on the handle, native drag and
drop), expand/collapse, remove (asks only for a row holding a value),
add with nested defaults, maxItems counter, empty state, collapse all;
every change edits form state only
- 422 row paths (content.3.answer) open the row, mark it and focus the
nested control; the banner no longer repeats row messages
- DropdownField dependsOn: waiting, loading, not used, empty, loaded with
depth indent, failed with retry; cached per sibling value, last request
wins, a stale value is cleared after a sibling change and announced
- FormView provides form feedback (errors and notices by row path, the
polite live region, its confirm dialog) and the options cache; FormGrid
provides the sibling scope for repeater rows
- repeater.* and form.dependent_* phrase keys (en, pl), smoke specs, docs
- RichEditorField: Reka toolbar, link popover with the http/https/mailto/tel
and relative URL policy, Edit HTML view, lossy note, skeleton while the
lazily imported TipTap chunk loads, HTML view fallback when it fails
- untouched values are submitted byte-identical; after an edit the value is
the editor HTML (empty paragraph -> ""); a value from outside (the save's
sanitized answer) reloads the editor
- read-only renders through a client display allowlist, never as stored
- TipTap 3.31.4 (core, pm, starter-kit) pinned exact after the package check
- richeditor.* phrase keys (en, pl), smoke specs, forms docs
- mlLocale: type predicates, localeRecord, the ML_LOCALE_CHANGE broadcast
- MLField: locale switch, whole-map emit, broadcast follow, content-locale
- FileCaptionModal: prefill, save via FileRoutes.update, 422 stays, danger toast
- ApiTokensPanel and ApiTokenCreateDialog mounted directly next to the app tests
Assert submit stays disabled with no scopes; keep the 422 focus path
with a selected scope so check-phase16.3.sh --all stays green.
Co-authored-by: Cursor <cursoragent@cursor.com>
Client-side canSubmit now matches the 422 rule so Submit stays disabled
with an empty scope set (WR-02).
Co-authored-by: Cursor <cursoragent@cursor.com>
Pin list states, row anatomy, revoke paths and create-dialog step 1/2
behaviours from the UI-SPEC, including plaintext lifetime backstops.
Co-authored-by: Cursor <cursoragent@cursor.com>
- fieldTypes: one load per script URL, failed and refused scripts
reported, global styles enabled on every screen, empty list on failure
- registry, formState, FormField and ListView: plugin renderers, locale
maps, notices as literal text, unknown column types as text
- AppShell: the View site link's href, target, rel and en/pl label
- TestFieldTypeUntouched: served values skip Decode, writes and notices;
one changed locale writes and reports only that locale; canonical JSON
comparison; a create never skips
- PluginField: an element that echoes its value on connect is never
written a second time
- MLPluginField wraps PluginField in the MLField locale switcher; the
element gets the active content locale and that locale's string
- isMLField treats a translatable plugin field as a locale map in the form
state, FormView adopt and the relation child modal
- summer-upload posts the file with the admin session to the field upload
route and answers summer-upload-result {id, url} or {id, error}
- noticeLines shows meta.notices under each field as a warning status line
(text only) and in the saved toast; the stored value is reloaded
- docs/backend/field-types.md documents the full element contract;
modules/boardwalk/dist rebuilt
- loadFieldTypes fetches GET /field-types after sign-in, loads each script
once and adds the styles as global links activateStyles never disables
- PluginField creates the type's element, sets the value property and the
contract attributes, takes summer-change as the form value and never
writes back a value the element just emitted
- rendererForField and isRegisteredField route any field with an element
to PluginField in forms, previews and the save payload
- rebuilt modules/boardwalk/dist
- Store list queries under summer-admin.list:<admin id>:<controller>; me()
and setCurrentUser set the owner, which removes other admins' keys and
the unscoped keys of older builds; nothing is read or written without one
- clearUser removes every stored query, so logout, session expiry and an
unrecoverable 401 (expireSession, now used by main.ts) all clear them
- A list request built from a restored query that fails for any reason
(422, 500, network) forgets the query and loads the plain list once
- A failing URL query keeps the error and offers a reset button
(backend::lang.list.reset_list, en and pl)
- Document remembered list state in docs/backend/lists-and-filters.md
- Rebuild modules/boardwalk/dist
- Form back, cancel, save-and-close and delete land on the list with the stored query; every list request after the return carries it
- listState helpers: key prefix, malformed stored values removed, empty query stores nothing, throwing storage
- ListView entry: URL wins, cleared controls forget the query, same-list navigation, 422 forgets, page clamp
- Logout removes every list key and keeps the sidebar preference
- listState.ts stores each list's query (search, sort, dir, page, per_page,
filter[name]) under summer-admin.list:<controller>; an empty query is
never stored, malformed stored values are dropped like a hand-edited URL
- ListView restores the stored query into the URL before its first list
request when entered without list params; a URL with list params wins
- a 422 forgets the stored query; an out-of-range page clamps to last_page
- logout clears every stored list query
- check-phase10.sh allows browser storage in useSidebar.ts and listState.ts
- list smoke tests start from a clean list state
- rebuilt modules/boardwalk/dist
- account_internal_test: metadata subtitle reader and writer (NULL, blank,
null, [], invalid JSON, non-objects, key preservation, NULL on the last
key), the attachOne contract, avatar cap, unique-violation mapping,
English fallbacks, and 401 for non-backend principals on all three routes
- account_test (Postgres): profile payload incl. copied WinterCMS metadata
and avatar rows, trimmed profile saves and metadata keys, the forbidden-key
allow-list with unchanged rows, email rules, password rules, the failed
attempt log, Bearer and cookie session effects, CSRF, avatar upload,
refusals (SVG, HTML as PNG, size, part shape, 413, no bucket) with no row
or blob left, replace and remove with blob cleanup
- lagoon: TestBackendAdminMetadataColumn covers type, Winter row round trip,
rollback of only the column and re-migrate
- SPA: AccountView, the user menu account item and avatar image,
setCurrentUser and profileInitials; lang fixture gains myaccount keys
- lagoon: 202610090001_backend_users_metadata adds WinterCMS's nullable
jsonable backend_users.metadata column (TEXT); rollback drops only it
- cabana.BackendUser.Metadata and Subtitle read metadata.subtitle leniently
(NULL, blank, null, [], invalid JSON and non-strings read as empty)
- GET /auth/me returns subtitle; PUT /account edits the signed-in admin's
own names, email and subtitle, and the password with current_password;
any other key is a 422 and nothing is written
- email: required, valid, 6-255 chars, unique against other admins' email
and login ignoring case (soft-deleted included); a unique-index race is 422
- a password change sets tokens_valid_after, revokes the presented token and
gives a cookie session a fresh cookie
- swag annotation, regenerated admin.json and schema.d.ts; route inventories,
CSRF walk (27) and OpenAPI conformance learn the route
- SPA: My account item in the user menu opens /account (AccountView), which
saves through PUT /account and updates the top bar via setCurrentUser
- backend::lang.myaccount strings (en, pl), rebuilt boardwalk dist
- cabana and lagoon READMEs, users-and-permissions and admin-spa docs
The record heading, breadcrumb crumb and document title took the first
plain text field only, so records named by an mltext field fell back to
the generic update message. A shared recordDisplayName helper now takes
the first text or ML field and reads its default locale, then the first
non-empty one.
A settings entry with a controller opens that controller's route from
its card (settingsPath); singleton cards still open /settings/<code>.
On a linked controller with no navigation entry of its own the rail
marks Settings current, the breadcrumbs read Settings > entry label
(plus the record title on record and create routes) and the page title
uses the entry label. The settings docs describe the admin behaviour;
the embedded admin shell is rebuilt.
pact.SettingsItem gains an additive Controller field, the equivalent of a
WinterCMS registerSettings 'url' => Backend::url(...) entry. A link entry
declares no Model, Form or NewModel and needs no AdminFS; start-up fails
when it combines Controller with a singleton form or a model, or names an
unregistered controller. Settings codes stay one namespace.
GET /settings lists a link entry with its controller only when the
principal passes the item's permissions and may open the controller.
Registry.Setting never returns a link entry, so the singleton settings
endpoints answer 404 for its code. SettingsEntry carries controller,
empty for singletons; the OpenAPI document, generated SPA types and
settings fixture follow, and the pact and cabana READMEs and the
settings docs describe the link.
- MLField.vue is the one locale-switching wrapper; MLTextField,
MLMarkdownField and the new MLTextareaField are shells around it
- isMLFieldType replaces the inline ML type-name checks in formState,
FormView and RelationChildModal
- register mltextarea as a group-labelled control; rebuild dist
- formState: changedLocales and presetUpdates hold the preset-follow rules
for text and mltext pairs
- FormView applies presetUpdates on create, tracks per-locale hand edits of
ML fields and follows the active ML locale
- rebuilt modules/boardwalk/dist
- MarkdownField posts the source to POST /markdown/preview when Preview
opens and again 300 ms after a change while open; stale answers dropped
- the pane binds only data.html of a 2xx answer; a refusal is a text notice
- mlmarkdown previews the active locale and follows a locale switch
- .summer-markdown style kit restores headings, lists, code and tables
- vitest coverage, forms.md and rebuilt modules/boardwalk/dist
Copy overwrote the active locale with the other locale's often-empty string, so the editor went blank. Keep a single synchronized locale picker.
Co-authored-by: Cursor <cursoragent@cursor.com>
WR-02 proofs and T-14.2.1-19..23 must fail the phase closed if hydration or child locale validation is removed.
Co-authored-by: Cursor <cursoragent@cursor.com>
MLMarkdownField and relation-child forms share the enabled-locale
inject and adopt merge; OpenAPI, types, docs, and boardwalk dist match.
Co-authored-by: Cursor <cursoragent@cursor.com>
Form schema meta lists enabled locales, Show/save expand mltext maps
without D-11 fallback, and the SPA seeds and adopt-merges every locale.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Document markdown, mltext, and mlmarkdown plus TranslationWriter save semantics
- Extend the SPA registry tests and rebuild committed boardwalk dist
Co-authored-by: Cursor <cursoragent@cursor.com>
Lift locale maps before ProjectWritableFields so a Journal-shaped save can persist the default host scalar and non-default locales through TranslationWriter without dropping nested JSON.
Co-authored-by: Cursor <cursoragent@cursor.com>
- WidgetField posts the summer-action event's detail.payload as payload only
when the detail carries one; a payload-less post body is unchanged
- after a successful action the response data is set on the element as the
data attribute (removed when the answer has none) and announced with a
summer-result event carrying {data, fill, message}; failures dispatch nothing
- WIDGET_RESULT_EVENT exported from formContext; unit tests for both directions
- modules/boardwalk/dist rebuilt
Ported plugins send Winter icon-* class names on nav and settings; the SPA
never loads Font Awesome, so unknown names rendered as empty squares. Map
BM Studies, Quizzes, icon-pencil, and a closed Winter backend alias table
onto named @lucide/vue 1.17.0 exports, keep Square for unknown names, and
rebuild the embedded boardwalk dist.
- list: the bulk menu in the toolbar and on the list view with its three failure rows, row states and invisible columns in the table
- form: the forbidden banner, password and preset on the form view, locked relation options, the save body of password and permission fields
- preview view: context fields, hidden tabs, the status hint, the footer with zero, one and several actions, load failures
- routing: the preview route and mapWinterUrl
- backstops: focus returns to the bulk menu trigger; preview URL mapping and the route replacement; locked relation options
- the slug preset runs on the table the user plugin's slug test uses
- BulkActionsMenu, RowStateBadges, RecordActions, PreviewField, FormErrorBanner, PasswordField and PermissionEditorField, each mounted on its own
- backstop: a row state outside the fixed set renders no badge and no class
- backstop: the permission editor's emission rules per mode, the locked row and codes outside the options
- FieldRelationContract.WritableForeignKey makes a belongsTo field over a
protected foreign key writable; the protected key list is unchanged
- cabana.RelationLockProvider names related ids an administrator may not add
or remove: options and labels carry locked, and a create or update that
changes the locked subset is 403 before any row is written
- columns.yaml invisible keeps a column searchable and out of the rows
- a controller implementing pact.FilterOptions serves a scope filter's
choices before the model
- SPA: locked chips and options in RelationField, DataTable skips invisible
columns
- README, docs, OpenAPI document, TS types and dist updated
- type: permissioneditor with mode radio (1, -1) or checkbox (1); the
controller serves the options per request through
cabana.PermissionEditorProvider and reads and stores the values
- a save answers 422 for a non-object, an unknown code or a value outside the
mode's set and 403 for a changed locked code; stored codes that are not
offered are kept
- record responses carry the stored permissions as an object
- SPA: PermissionEditorField with sections by tab, locked rows and a read-only
mode for the preview
- README, docs, OpenAPI document, TS types and dist updated
- pact.FormVirtualFields lists form fields that are not model columns: never
bound, filled or projected; their values reach the Form hooks through
cabana.VirtualFieldsFromContext when the field's context allows the operation
- type: password is a masked field that must be listed as virtual
- pact.FormRules supplies the rule set per operation and replaces the model's
Rules() for admin saves; a rule on a virtual field sees the submitted value
- preset on a text field follows another text field on the create form
- SPA: PasswordField, preset handling in FormView, empty password left out of
an update
- README, docs, OpenAPI document, TS types and dist updated
- config_form.yaml preview block (optional headerPartial), reported in the form schema as preview
- fields with context: preview show only on the preview screen and are never written
- form messages preview and edit; recordActions without a preview block stops boot
- SPA route {id}/preview, PreviewView and PreviewField, record actions in the footer
- mapWinterUrl maps preview/:id; the update form returns to the preview
- summer-callout partial style classes for status hints
- README, docs, OpenAPI document, TS types and the embedded build updated
- hooks and bulk, record, toolbar and widget actions may return it
- 403 forbidden with the localized message and field details; the write's
transaction is rolled back; other errors stay the opaque 500
- form shows a refused save as a persistent banner and keeps the values;
a refused delete is a toast
- smoke tests, OpenAPI notes, dist, README, docs
- pact.ListRowStates with the fixed RowState set deleted, negative, disabled
- list response meta.row_states keyed by row id; unknown values dropped
- list messages rowStateDeleted, rowStateNegative, rowStateDisabled
- update writes through the scope the load used, so a soft-deleted record
a controller includes stays soft-deleted
- DataTable row state badges and text styles
- roster fixture, smoke tests, OpenAPI, TS types, dist, READMEs, docs