- ReorderView reads flat from the reorder data; ReorderBranch takes a flat
prop and shows only move up and move down
- ArrowRight and ArrowLeft do nothing, an inside drop lands after the row
(tree.ts flatZone, flatMove) and flatten sends parent_id null
- vitest flat cases; docs describe the flat page; rebuilt dist
- string mode: split on the separator (comma or whitespace), normalised,
stored joined by a comma or one space; null stores an empty string
- customTags false refuses a tag outside options (taglist.not_allowed)
- taglist checked and hydrated on relation forms and settings forms;
settings bind taglist columns; required checked on the tags
- admin: TaglistField string mode and closed option lists, preview renders
the tags read-only as a comma-separated list; rebuilt dist
- docs: Taglist section covers both modes, separator and customTags
- cabana: core type taglist (mode string|array, separator, customTags,
options as a list of tags), column type gate, save-time normalisation
(trim, drop empties, dedupe) and taglist.invalid 422, record hydration
- FormField gains separator and customTags (omitempty); OpenAPI and
schema.d.ts regenerated
- admin: TaglistField chip input with suggestions, registry entry, vitest
- acme.catalog fixture with TestTaglistFieldCompile and TestTaglistFieldSmoke
- docs: forms.md Taglist section, cabana README; rebuilt dist
- type: password on a settings form binds a lagoon.Encrypted column as a
write-only secret (any other column fails boot naming the field): the
GET omits it from data and reports stored, env or empty per field in
meta.secrets; on PUT an absent key or "" keeps the value, null clears
it, a non-string is a 422; the answer never carries a value
- cabana.SecretFallbacks (field to config key) gives the env state;
cabana.SettingsMeta, SecretStored/SecretEnv/SecretEmpty exported
- settings forms accept richeditor: the Put sanitizes through seaglass
like a controller save and returns meta.notices; repeater and
dependsOn stay refused (settingsFormOnly next to controllerFormOnly)
- backend::lang secret.* phrases in en and pl
- admin SPA: PasswordField secret status line (stored with Remove stored
value, env, empty, pending clear with Undo), SettingsFormView tabs with
error-tab activation and the secrets/notices meta, settings payload
that never sends an untouched secret; regenerated OpenAPI and TS
types; rebuilt dist
- vault fixture, TestSettingsSecretCompile, Docker TestSettingsSecretSmoke
and TestSettingsRichEditorSmoke, conform settings fixture with a
password field; settings.md sections, forms.md notes, cabana README
- columns.yaml type: money with currency or currencyFrom (exactly one),
never searchable, not sortable by default; every list row carries the
currencyFrom column's value under its own key; the schema serves
currency and currencyFrom on the column
- pact.AdminRecordAction.Danger, served as danger on cabana.RecordAction
(absent when false); RecordActions.vue passes it to the confirm dialog
- CellValue money kind through formatMoney with the row's code, the em
dash when empty and the amount alone without a code; DataTable aligns
money headers and cells to the right
- PreviewView renders Edit only when a registered value field allows
update (F6)
- main.css .summer-table* and .summer-stat__note kit classes (F7) over
--c-* variables; cabana README kit rows
- fixture record actions void (danger) and resend; TestMoneyListCompile,
TestMoneyListSmoke and TestRecordActionDangerSmoke; regenerated OpenAPI
and TS types; rebuilt dist; lists-and-filters and admin-controllers docs
- type: money on fields.yaml with currency or currencyFrom, scale (2..4,
default 4) and max; boot fails naming the field on two sources, none,
a bad scale or max, a column that is not a string or a Scanner/Valuer,
or a currencyFrom that is not a string column
- a save answers 422 money.invalid, money.too_precise or money.too_large
on the field and fills the accepted decimal string unchanged; the same
check runs on controller, settings and relation child saves
- backend::lang money.* phrases in en and pl
- admin SPA: money.ts (parseMoneyInput, trimMoneyDisplay, formatMoney over
Intl with the string itself), MoneyField.vue with the currency suffix,
blur validation and the read-only box, preview through the control,
the field scope on PreviewView; regenerated OpenAPI and TS types,
rebuilt modules/boardwalk/dist
- acme.payments fixture with TestMoneyFieldCompile and the Docker
TestMoneyFieldSmoke; forms.md Money section and cabana README
- 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
- 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
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>
- 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
- 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.
The admin sets the html font size to 14px, so the rem utilities w-11 and
size-5 shrank the track to 38.5px and the knob to 17.5px against the
fixed 26px height and 18px travel. Use w-[44px] and size-[20px] so the
knob sits in a symmetric 3px inset at both ends. Rebuild the embedded
admin shell.
- 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>
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>
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
- 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
- pact.HasAdminRecordActions with AdminRecordAction (Applies, Run)
- config_form.yaml recordActions, compiled fail-loud
- show response meta.actions lists the permitted actions that apply
- POST .../{controller}/{id}/actions/{action}: record loaded and locked
through the form scope; 404 out of scope, 409 when it does not apply
- RecordActions.vue with confirm and request flow (mounted by plan 02)
- roster fixture, smoke tests, OpenAPI, TS types, 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>
The partial-segment check read only DateField segments, so a time-mode
field (Reka TimeField, data-reka-time-field-segment) with some segments
typed was never flagged. Read both segment attributes. DatepickerField
tests cover both UI-SPEC backstops (Escape focus and disabled days, the
fixed-zone datetime round trip) and the partly filled time case.
- 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
- pin @internationalized/date 3.12.4 as a direct admin dependency (approved)
- dateFormat.ts parses and emits date, datetime (local display, UTC emit,
ignoreTimezone wall clock) and time values without the global Date
- DatepickerField on Reka DatePicker and TimeField with locale segments,
calendar popover, clear button, min/max and yearRange bounds
- list cells of type date and time render the stored string
- datepicker 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