- 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
- 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
- cabana.BackendUser is an attach.Owner (MorphName Backend\Models\User,
cabana.BackendUserMorphType) with one public attachOne avatar
(cabana.BackendUserAvatarField), as WinterCMS stores it
- POST /account/avatar stores one file_data image (jpg, jpeg, png, gif or
webp, image guard, at most cabana.AvatarMaxBytes = 2 MiB, under a
server-generated name) and replaces the previous avatar: old rows are
deleted in the transaction, their blobs after the commit
- DELETE /account/avatar removes the avatar and is idempotent
- GET /auth/me and the account routes return avatar {url, thumb_url}
(250x250 crop); cabana.BackendUserAvatar lets plugins read it
- swag annotations, regenerated admin.json and schema.d.ts; route
inventories, CSRF walk (29) and OpenAPI conformance learn both routes
- SPA: avatar upload and remove on My account, avatar image in the user
menu trigger and header; backend::lang.myaccount avatar strings
- rebuilt boardwalk dist; cabana README, users-and-permissions and
admin-spa docs
- 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.
- 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
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>
- 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
- pact.HasAdminBulkActions with AdminBulkAction, its input and result
- config_list.yaml bulkActions, compiled fail-loud, needs showCheckboxes
- POST .../{controller}/bulk/{action}: ids resolved and locked through the
list scope in one transaction; partial selection is 409
- list schema offers declared actions per principal, with confirm text
- admin SPA bulk actions menu with confirm, busy state and failure toasts
- acme.roster fixture, tracer test, OpenAPI, TS types, dist, READMEs, docs
BM UAT on v0.1.1 showed a date-only calendar for datetime fields, a stuck discard dialog, and raw ISO timestamps when columns.yaml omitted type. The picker now edits time in the popover, confirm sits above the calendar, and omitted time.Time / Date / TimeOfDay columns compile as datetime / date / time.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep form save behind in-flight uploads, make retries idempotent via X-Upload-Id, cap remaining JSON bodies, and surface pending pivot type errors instead of zeroing them.
Co-authored-by: Cursor <cursoragent@cursor.com>
- RelationManager renders create/link/delete/unlink in declared order with
one primary, opens the child modal (update or view form) or the pivot
modal on row click, and deletes selected children behind a busy confirm
- RelationChildModal creates and edits children with its own session key
(X-Child-Session-Key) so uploads and dates work inside it
- RelationPivotModal edits link details; the picker links one record with
its pivot values when the relation has a pivot form
- deferrable managers render on the create screen with owner id 0, the
form's X-Session-Key and the pending note, and mark the form dirty
- the registry resolves RelationManager lazily (child forms close an
import cycle); DataTable gains openable rows and a trailing cell
- relation lang keys in en and pl; rebuilt boardwalk dist
- sessionKey.ts: one 32-byte base64url key per form mount, sent only in headers
- api/files.ts: FileRoutes over the record and child file routes, XHR upload with progress, 401 refresh and retry
- FileuploadField and FileCaptionModal per UI-SPEC section 3: dropzone, image grid, rows, per-item states, client pre-checks, reorder, protected previews
- FormView provides FORM_SESSION, counts pending changes as dirty and sends X-Session-Key on create and update
- fileupload lang keys in en and pl, admin-spa docs note, deferred smoke test, rebuilt dist
- ListToolbar renders server-filtered actions after delete as outline buttons, busy during their POST
- ListView posts {} to toolbar/{action}, toasts, reloads the list and refetches the header partial
- Lists load controller assets too, so other controllers' stylesheet links are disabled on every open
- Vite dev server proxies {prefix}/assets to summer serve; dist rebuilt
- partialNodes rebuilds the server node tree with h() under the server's tag, attribute and URL lists
- PartialHost owns the skeleton, empty and failure states and keeps nodes visible on refetch
- type: partial is a valueless group-labelled field rendered on create and update
- ListView shows headerPartial above the list card and refetches it after bulk delete
- summer-partial and summer-stats style kit in main.css, documented in the cabana README; dist rebuilt
- pluginAssets loads controller scripts and stylesheets from {base}/assets/ only, once per URL
- WidgetField mounts the custom element with attributes only and posts summer-action through the typed client
- Only declared fill keys returned by the server are patched; the form turns dirty and nothing saves
- widget is a registered valueless type rendered on create and update, labelled as a group
- backend::lang.extension strings in en and pl; embedded dist rebuilt
- 41 unit and component suites under admin/tests/{app,state,shell,list,form,relation,views,ui}
covering states and a11y roles; every src module is imported by a test
- typed fixture helper assigns each JSON fixture to its generated OpenAPI type
- fix: iconFor ignores inherited object members such as "constructor"
- fix: field controls import ./control instead of the registry (import cycle
left a renderer unregistered depending on module load order)
- fix: dropdown shows the placeholder for an unknown stored value next to an emptyOption
- fix: list announces a failed schema load even when the rows arrive after it
- tailwind no longer scans admin/tests; boardwalk/dist rebuilt
- useSidebar: collapsed below 1100px (matchMedia) or by the admin's choice,
persisted as a boolean under summer-admin.sidebar; the viewport never
overwrites the stored choice
- SectionPanel collapse and rail expand buttons; SectionFlyout (role menu)
opens on hover, focus, Enter or ArrowDown on a rail item, closes on Esc or
about 200 ms after leaving and returns focus to the rail item
- Breadcrumbs with plugin, controller and record crumbs; UserMenu (Reka
DropdownMenu) with initials, name, role and Wyloguj
- useAuth.logout POSTs /auth/logout, clears user, navigation and settings
and routes to login even when the call fails
- applyColorScheme toggles .dark from prefers-color-scheme (no toggle, A6)
- newest toast first, 200 ms fade and scale for dialogs and toasts
- one relation-manager type constant in the registry
- shell smoke tests; tests default to a desktop, light matchMedia
- relation-manager registered in the field registry; renders only on an
existing record, never on create, and is never part of the save body
- RelationManager: relation schema label and comment, debounced search,
selectable linked list (DataTable relation variant), toolbar buttons in
declared order, confirmed unlink with plural messages and toasts
- RelationPickerModal: Reka Dialog (aria-modal, focus trap, Esc) over the
candidates endpoint five per page, selection kept across pages, Dodaj (N)
POSTs link, focus returns to the opener
- admin OpenAPI documents search, sort, dir, page and per_page on the linked
and candidate relation routes so the SPA sends them typed
- neutral acme.demo.widgets members fixtures and relation smoke tests
- FormTabs groups fields by tab (untabbed fields in the default tab) as a
segmented tablist; a tab holding invalid fields after a 422 shows a
count badge and the form switches to the first invalid field
- switch, checkbox and relation join the renderer registry: toggle cards
(role=switch, 20px checkbox) keep a numeric value numeric; relation
fields are read-only labels from meta.labels, a searchable single
select over fields/{field}/options with emptyOption first, or ordered
removable chips with an appending search (300 ms debounce, 20 per
page, more on scroll or the more action)
- FormView gets the back button, record title and update subtitle, a
sticky footer (Usuń with deleteConfirm then DELETE, Anuluj, Zapisz i
zamknij to the mapped redirectClose, Zapisz), and asks before leaving a
dirty form on any route change plus a beforeunload guard
- Settings: the rail pins Ustawienia to the bottom when /settings is
non-empty; /settings lists pages by category; /settings/:code renders
the settings schema through FormGrid and the registry, PUTs the values
and maps a 422 like the record form
- New backend::lang keys (form.more_options, tab_default, discard,
settings.back); form and settings smoke tests; boardwalk/dist rebuilt
- List state (search, sort, dir, page, per_page, filter[<name>]) lives in
the URL query through parseListQuery/toListQuery; every change is a
router replace and clears the selection; search is debounced 300 ms and
resets the page
- DataTable renders the schema columns with a tri-state page checkbox,
asc/desc/none sorting with aria-sort, selected rows, a sticky header,
eight skeleton rows while loading and an empty slot
- CellValue renders text (muted dash when empty, arrays comma-joined),
datetime as YYYY-MM-DD HH:mm and switch as the Tak/Nie pills
- The heading shows the plural recordCount and the create button;
delete sits in the toolbar, disabled without a selection, and confirms
with the plural deleteConfirm in a Reka alert dialog before POSTing
bulk-delete; a 409 shows a danger toast
- FilterBar renders switch (JSON of the option value), daterange
(from..to) and scope filters (choices from filters/{scope}/options)
- Pagination shows the range, the per-page select over perPageOptions
(hidden with one choice) and a pager with ellipsis
- The tracer smoke test skips the new checkbox column; new backend::lang
list keys; boardwalk/dist rebuilt
- The SPA loads the backend::lang bundle before /auth/me, sets the
document language from meta.locale and renders plural messages with
Intl.PluralRules; interpolate mirrors phrasebook for :name/:Name/:NAME
- Create and record routes; mapWinterUrl maps recordUrl and redirects
onto the controller's list, create and record routes only
- List rows open their record; FormView loads the form schema and the
record, shows context-allowed fields in the span grid, saves values
keyed by field name and toasts the resolved saved message
- A 422 puts each message under its field (aria-invalid,
aria-describedby), shows the plural banner, focuses the first invalid
field in schema order and clears a field's error on change
- The D-05 registry maps text, textarea, number and dropdown; any other
type renders the unsupported-field box with the type in DM Mono
- The admin OpenAPI document declares the write request bodies
(AdminRecord, AdminIDsRequest) and the list filter query as a
deepObject, so the typed client can send them
- New backend::lang form.load_failed key; boardwalk/dist rebuilt
- backend.uri prefix (default /backend) mounts the admin API at {prefix}/api/v1
and the embedded SPA shell at {prefix} with an api/ JSON 404 fallback
- cookie transport: an X-Requested-With login sets the HttpOnly summer_admin
cookie and returns no token; the backend guard reads the cookie after Bearer
- CSRF wrapper refuses cookie-only POST/PUT/DELETE without X-Requested-With
- boardwalk package embeds boardwalk/dist, rewrites index.html once per prefix
and sets cache and security headers
- framework admin OpenAPI pipeline (swag, swagger2openapi, openapi-typescript)
with prefix-relative paths and typed envelopes for the tracer routes
- admin/ Vite SPA: login, plugin rail, section panel and read-only list
through the openapi-fetch client typed by the generated schema