docs(quick-261006-t5r): preset on mltext fields
This commit is contained in:
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-09-16)
|
||||
Phase: 15 (Journal plugin) — EXECUTING
|
||||
Plan: 4 of 4
|
||||
Status: Ready for phase verification
|
||||
Last activity: 2026-10-06 — Completed quick task 261006-sne: Render Markdown field Preview via sanitized cabana.RenderMarkdown
|
||||
Last activity: 2026-10-06 — Completed quick task 261006-t5r: Make SPA slug preset follow mltext source fields
|
||||
|
||||
Progress: [██████████] 100%
|
||||
|
||||
@@ -647,6 +647,7 @@ Recent decisions affecting current work:
|
||||
| 261006-s0v | Remove copy-from locale UI from ML text and markdown admin fields | 2026-10-06 | 2f03128 | [261006-s0v-remove-copy-from-locale-ui-from-ml-text-](./quick/261006-s0v-remove-copy-from-locale-ui-from-ml-text-/) |
|
||||
| 261006-seq | Fix nullable pointer scalars rendering as <nil>/address in cabana ML form fields | 2026-10-06 | 90b87d6 | [261006-seq-fix-nullable-pointer-scalars-rendering-a](./quick/261006-seq-fix-nullable-pointer-scalars-rendering-a/) |
|
||||
| 261006-sne | Render Markdown field Preview via sanitized cabana.RenderMarkdown | 2026-10-06 | a0116df | [261006-sne-render-markdown-field-preview-via-saniti](./quick/261006-sne-render-markdown-field-preview-via-saniti/) |
|
||||
| 261006-t5r | Make SPA slug preset follow mltext source fields | 2026-10-06 | b5d8506 | [261006-t5r-make-spa-slug-preset-follow-mltext-sourc](./quick/261006-t5r-make-spa-slug-preset-follow-mltext-sourc/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
---
|
||||
phase: quick-261006-t5r
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- modules/cabana/form_schema.go
|
||||
- modules/cabana/preset_ml_test.go
|
||||
- modules/cabana/phase121_form_test.go
|
||||
- modules/cabana/phase121_schema_boot_test.go
|
||||
- admin/src/components/form/formState.ts
|
||||
- admin/src/views/FormView.vue
|
||||
- admin/tests/form/formState.test.ts
|
||||
- admin/tests/form/FormView.test.ts
|
||||
- modules/boardwalk/dist/
|
||||
- modules/cabana/schema_types.go
|
||||
- modules/cabana/admin_openapi.go
|
||||
- admin/openapi/admin.json
|
||||
- admin/src/api/schema.d.ts
|
||||
- modules/cabana/README.md
|
||||
- docs/backend/forms.md
|
||||
autonomous: true
|
||||
requirements: [QUICK-261006-t5r]
|
||||
|
||||
estimate:
|
||||
tokens: 70000
|
||||
raw_tokens: 70000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A fields.yaml with an mltext title and `slug: { type: mltext, preset: { field: title, type: slug } }` compiles and boots; the form schema serves the slug field as type mltext with preset {field: title, type: slug}"
|
||||
- "text -> mltext and mltext -> text presets are accepted too; a preset on any other target type (mlmarkdown included) or from a source that is neither text nor mltext still stops the start-up, with messages that say text or mltext"
|
||||
- "On the create form, typing in locale L of an mltext source writes the slug (or exact copy) of that locale's text into locale L of an mltext target, and leaves the target's other locales as they are"
|
||||
- "Editing locale L of an mltext target by hand (clearing included) stops only locale L from following; the target's other locales keep following the source; on the update form nothing follows"
|
||||
- "An mltext source drives a text target from the source's active-locale string; a text source writes only the active locale of an mltext target"
|
||||
- "The existing text -> text preset behaviour and its tests are unchanged"
|
||||
- "modules/boardwalk/dist matches a fresh build; admin.json and schema.d.ts match a fresh generation; TestDocsTree, go vet ./... and go test ./... are green"
|
||||
artifacts:
|
||||
- path: modules/cabana/form_schema.go
|
||||
provides: "compilePresetKey accepts type text or mltext; checkPresets accepts a text or mltext source"
|
||||
- path: modules/cabana/preset_ml_test.go
|
||||
provides: "DB-free CompileForm + checkPresets tests for every text/mltext pairing and the refusals"
|
||||
- path: admin/src/components/form/formState.ts
|
||||
provides: "changedLocales and presetUpdates: the pure preset-follow rules for text and mltext pairs"
|
||||
- path: admin/src/views/FormView.vue
|
||||
provides: "update() applies presetUpdates on create, tracks per-locale edits of ML fields and the active ML locale"
|
||||
- path: modules/boardwalk/dist
|
||||
provides: "Rebuilt embedded admin SPA"
|
||||
key_links:
|
||||
- from: admin/src/views/FormView.vue update()
|
||||
to: admin/src/components/form/formState.ts presetUpdates
|
||||
via: "on create, every source update merges presetUpdates(fields, name, previous, value, values, mlLocale, isEdited) into the next values"
|
||||
- from: admin/src/components/form/fields/MLTextField.vue broadcastMLLocale
|
||||
to: admin/src/views/FormView.vue mlLocale
|
||||
via: "window ML_LOCALE_CHANGE event; FormView follows it, starts on the first enabled locale and resets to it when the active tab changes (FormGrid remounts panel controls)"
|
||||
- from: modules/cabana/form_schema.go compilePresetKey/checkPresets
|
||||
to: admin/openapi/admin.json
|
||||
via: "schema_types.go comments and the admin_openapi.go description regenerated by scripts/check-admin-openapi.sh"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Make the fields.yaml `preset:` (slug follows title) work when the source and/or the target is an `mltext` field.
|
||||
|
||||
Bug (Journal UAT, proof host sm-grzybyfunkcjonalne-app, /backend, admin/admin): on create the post slug does not follow the title. Two framework gaps cause it: the server refuses `preset` on anything but `type: text` (and refuses a non-text source), and the SPA's FormView.update only runs the preset when `field.type === 'text'`. Journal's post title and slug, and its category name and slug, are all `mltext`.
|
||||
|
||||
Purpose: the framework supports presets on multilingual fields with the same slug/exact rules, create-only scope and stop-on-manual-edit rule as text fields; ML pairs follow per locale.
|
||||
Output: cabana accepts mltext presets (Go code + tests), the SPA follows them (pure helpers + FormView wiring + Vitest), the rebuilt dist, regenerated OpenAPI/types, README and docs. Three code commits. The sm-journal-plugin YAML is NOT edited here; the snippet it needs is recorded below and must be copied into the SUMMARY.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||
@~/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/STATE.md
|
||||
@CLAUDE.md
|
||||
@modules/cabana/form_schema.go
|
||||
@admin/src/views/FormView.vue
|
||||
@admin/src/components/form/formState.ts
|
||||
@admin/src/components/form/fields/MLTextField.vue
|
||||
|
||||
Facts gathered during planning (do not re-derive):
|
||||
|
||||
- Server: `compilePresetKey(typ, values, field)` (form_schema.go ~line 645) returns "preset is only valid on type: text" when typ is not text. `checkPresets(fields)` (~line 691, called only from extension.go compileExtension at boot, NOT from CompileForm) returns "field %s: preset field %s must be a text field" for a non-text source. Settings forms (settings.go) and relation forms (relation_form.go) refuse `preset` entirely; that stays.
|
||||
- Existing Go assertions of those two messages: phase121_form_test.go TestPresetSchema rows "not a text target" and "source is not text" (~lines 375, 377); phase121_schema_boot_test.go TestPhase121BootErrors rows "preset on a field that is not text" and "preset from a field that is not text" (~lines 156-161). Both suites boot the roster fixture on testcontainers Postgres.
|
||||
- DB-free compile pattern (ml_test.go TestMLFieldTypes): `CompileForm("acme.demo", schemaController{model: "Widget"}, formFS(formConfig, yaml))` in package cabana; formFS and formConfig live in form_schema_test.go. mltext fields compile there without a model column.
|
||||
- SPA form state: an mltext/mlmarkdown value is a locale map, e.g. `{ en: '', pl: '' }` (initialValues seeds every enabled locale on create; adopt merges via mergeMLValue). MLTextField shows ONE locale at a time (`activeLocale`, initialised to the first enabled locale, 'en' when none) and emits the WHOLE map with only the active locale changed. Locale changes broadcast on window as `ML_LOCALE_CHANGE` ('summer:ml-locale', re-exported from formState.ts); every ML control follows it. FormGrid renders only the active tab's fields keyed by name with no KeepAlive, so switching tabs remounts ML controls on the first enabled locale.
|
||||
- FormView.update(name, value) today: builds `next`, adds name to the `edited` Set, and on create rewrites every field whose `preset.field === name && type === 'text' && !edited.has(field.name)` with `presetValue(preset.type, value)`. Preset writes never go through update(), so they never count as edits. FORM_PATCH (widgets) also calls update().
|
||||
- The ML editor input id is `field-<name>-editor` (FormField controlId `field-<name>`, MLTextField editorId `${controlId}-editor`); the locale select is `[data-field="<name>"] [data-ml-locale]`. In FormView tests, `setValue` on that select fires change and broadcasts.
|
||||
- Test fixtures: rosterFormSchemaFixture (admin/tests/fixtures/roster.form-schema.json) has `name` text and `slug` text with `preset: {field: name, type: slug}`, `data.meta: { locale: "en" }` (FormMeta also allows `enabledLocales`). The existing FormView test "fills a preset target on create only, and stops at the first manual edit" (FormView.test.ts ~line 539) must stay green; its describe block defines `people()`, `field()` and `save()` helpers.
|
||||
- Build/check scripts (as in quick 261006-sne): `npm --prefix admin run build` writes modules/boardwalk/dist; `scripts/check-admin-dist.sh` is the dist drift gate; `scripts/check-admin-openapi.sh` (no args) regenerates admin/openapi/admin.json and admin/src/api/schema.d.ts from the swag annotations and Go doc comments, `--check` is its drift gate. Never hand-edit admin.json or schema.d.ts.
|
||||
- Docs: docs/backend/forms.md line ~108 ("`preset` on text fields"), ~112-123 (preset paragraph, YAML example, rules sentence ending "a source that is not a text field of the same form stops the start-up"), ML section ~328. modules/cabana/README.md line 28 (Features bullet) and line 225 (`cabana.FieldPreset` row). admin_openapi.go ~line 285 description says "`preset` on a text field ...". schema_types.go Preset field comment (~line 312) and FieldPreset doc (~line 323) say text field.
|
||||
- Commit rules: one logical change per commit, planning docs never in a code commit, NO co-author trailers (the user's global CLAUDE.md overrides any attribution reminder).
|
||||
|
||||
Discretion choices made in this plan (record them in the SUMMARY):
|
||||
- ML -> ML follows per locale: only the locales whose source text changed in this update are rewritten, each from its own locale.
|
||||
- Edits of an ML target are tracked per locale: a hand edit of locale L stops L only.
|
||||
- A preset never writes a locale the administrator neither typed in (source side) nor is looking at (target side): text -> mltext writes only the active locale; untouched locales stay empty so the server's translation fallback applies, and no unseen duplicate translations get saved.
|
||||
- mltext -> text follows the source's active-locale string (FormView tracks the active ML locale).
|
||||
- mlmarkdown stays out: the server admits text/mltext targets and text/mltext sources only.
|
||||
|
||||
Plugin follow-up (sm-journal-plugin, NOT edited in this task; copy into the SUMMARY verbatim). models/post/fields.yaml, the slug field gains a preset:
|
||||
|
||||
```yaml
|
||||
slug:
|
||||
label: golem15.journal::lang.post.slug
|
||||
type: mltext
|
||||
span: right
|
||||
tab: golem15.journal::lang.post.tab_edit
|
||||
required: true
|
||||
preset:
|
||||
field: title
|
||||
type: slug
|
||||
```
|
||||
|
||||
models/category/fields.yaml, the slug field gains `preset: { field: name, type: slug }` the same way (its source `name` is mltext). models/tag/fields.yaml already has a text -> text preset and needs nothing.
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer" tdd="true">
|
||||
<name>Task 1: cabana accepts preset on mltext fields (Journal title -> slug compiles, boots and is served)</name>
|
||||
<files>modules/cabana/preset_ml_test.go, modules/cabana/form_schema.go, modules/cabana/phase121_form_test.go, modules/cabana/phase121_schema_boot_test.go</files>
|
||||
<read_first>modules/cabana/form_schema.go (compilePresetKey, checkPresets), modules/cabana/ml_test.go (TestMLFieldTypes pattern), modules/cabana/form_schema_test.go (formConfig, formFS), modules/cabana/phase121_form_test.go (TestPresetSchema), modules/cabana/phase121_schema_boot_test.go (preset rows of TestPhase121BootErrors)</read_first>
|
||||
<precondition>A Docker daemon is reachable so the testcontainers Postgres used by TestPresetSchema and TestPhase121BootErrors can start.</precondition>
|
||||
<behavior>
|
||||
- Journal shape: title type mltext plus slug type mltext with preset mapping field title, type slug compiles with CompileForm; checkPresets on the compiled fields returns nil; the slug field's Preset is {Field: "title", Type: "slug"}; json.Marshal of that field contains "type":"mltext" and "preset":{"field":"title","type":"slug"}.
|
||||
- Short form `preset: title` on an mltext slug compiles with Type slug; a mapping with type exact compiles with Type exact.
|
||||
- Mixed pairs pass CompileForm and checkPresets: text source -> mltext target, mltext source -> text target.
|
||||
- `preset` on an mlmarkdown target fails CompileForm with an error containing "preset is only valid on type: text or mltext"; the same for a markdown target.
|
||||
- An mltext slug whose source is an mlmarkdown field passes CompileForm but checkPresets returns "field slug: preset field body must be a text or mltext field".
|
||||
</behavior>
|
||||
<action>
|
||||
RED first. Create modules/cabana/preset_ml_test.go (package cabana) with TestPresetML: table-driven subtests over fields.yaml strings compiled through CompileForm("acme.demo", schemaController{model: "Widget"}, formFS(formConfig, yaml)) followed by checkPresets(schema.Fields), covering every row of the behavior block (look the slug field up by name in schema.Fields for the Preset and JSON assertions). Run it and confirm the accept rows FAIL on the current code with the text-only messages.
|
||||
|
||||
GREEN, per the quick task (preset must work when source and/or target is mltext). In modules/cabana/form_schema.go: compilePresetKey accepts typ "text" or "mltext" and otherwise returns exactly "preset is only valid on type: text or mltext"; checkPresets accepts a source of type "text" or "mltext" and otherwise returns "field %s: preset field %s must be a text or mltext field". Update the doc comments above both functions to say text or mltext (an mltext pair follows per locale in the admin SPA). Do not touch settings.go or relation_form.go (both still refuse preset).
|
||||
|
||||
Update the four existing assertions to the new messages: TestPresetSchema rows "not a text target" and "source is not text" in phase121_form_test.go, and the two matching TestPhase121BootErrors rows in phase121_schema_boot_test.go. Keep their fixtures; only the wanted text changes.
|
||||
|
||||
Do not edit README, docs, schema_types.go or admin_openapi.go here (Task 3 does it with the OpenAPI regeneration, so no drift sits between commits).
|
||||
|
||||
Run gofmt, go vet and the targeted tests. Commit: feat(cabana): accept preset on mltext fields (code + tests only; no .planning files, no co-author trailers).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && bash -c 'test -z "$(gofmt -l modules/cabana)"' && go vet ./modules/cabana && go test ./modules/cabana -run 'TestPresetML|TestMLFieldTypes' -count=1 && go test ./modules/cabana -run 'TestPresetSchema|TestPhase121BootErrors' -count=1</automated>
|
||||
</verify>
|
||||
<done>TestPresetML failed before the change and passes after it; the Journal-shaped mltext title -> mltext slug preset compiles, passes checkPresets and serializes with type mltext and its preset; mlmarkdown/markdown targets and an mlmarkdown source are refused with the "text or mltext" messages; TestPresetSchema and TestPhase121BootErrors pass with the updated messages; one code commit.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: SPA follows presets on mltext fields per locale, with per-locale edit stop; rebuild dist</name>
|
||||
<files>admin/src/components/form/formState.ts, admin/src/views/FormView.vue, admin/tests/form/formState.test.ts, admin/tests/form/FormView.test.ts, modules/boardwalk/dist/</files>
|
||||
<read_first>admin/src/components/form/formState.ts (presetValue, localeRecord re-export, ML_LOCALE_CHANGE re-export), admin/src/views/FormView.vue (update, load, edited, onMounted/onBeforeUnmount), admin/src/components/form/fields/MLTextField.vue, admin/tests/form/formState.test.ts (preset values describe), admin/tests/form/FormView.test.ts (describe 'password and preset on the form' and its helpers), admin/tests/fixtures/typed.ts (clone, rosterFormSchemaFixture)</read_first>
|
||||
<behavior>
|
||||
- changedLocales({en:'',pl:''}, {en:'A',pl:''}) is ['en']; equal maps give []; a missing key counts as ''; a non-map previous (undefined, a string) is treated as an empty map.
|
||||
- presetUpdates, text -> text: identical to today (presetValue of the new source); an edited target is left out.
|
||||
- ML -> ML: source change {en:'',pl:''} -> {en:'Hello World',pl:''} gives target {en:'hello-world', pl:<unchanged>}; a later pl change rewrites only pl; when isEdited(target,'pl') is true a pl change returns nothing for that target while an en change still rewrites en; type exact copies the text; the target is absent from the result when no locale was written.
|
||||
- ML -> text: the target becomes presetValue of the source's activeLocale string ('' when that locale is missing); an edited target is left out.
|
||||
- text -> ML: only target[activeLocale] is written, sibling locales are preserved; when isEdited(target, activeLocale) the target is left out.
|
||||
- A preset on any other target type (for example mlmarkdown or checkbox) is ignored.
|
||||
- FormView, Journal case (name and slug both mltext, enabledLocales en/pl, create): typing 'Zażółć gęślą' in #field-name-editor shows 'za-g-l' in #field-slug-editor; switching the name locale select to pl and typing 'Witaj Świecie' shows 'witaj-wiecie' in the slug editor (now on pl); switching back to en shows 'za-g-l'; typing 'reczny' into the slug's pl editor and then changing the pl name keeps 'reczny'; changing the en name still updates the en slug; the POST body (422 reply) carries both locale maps with those values.
|
||||
- FormView, update mode with the same ML schema: changing the name does not touch the slug.
|
||||
- FormView, mltext name -> text slug: the slug follows the en name, then the pl name after switching the locale, and stops after a manual edit.
|
||||
- FormView, text name -> mltext slug: typing the name fills only the slug's active (en) locale; the slug's pl editor stays empty.
|
||||
- The existing test 'fills a preset target on create only, and stops at the first manual edit' passes unchanged.
|
||||
</behavior>
|
||||
<action>
|
||||
RED first: add the unit cases to admin/tests/form/formState.test.ts (a new describe next to 'preset values (D-27 G7)') and the FormView cases inside the existing 'password and preset on the form' describe in admin/tests/form/FormView.test.ts, reusing its people(), field() and save() helpers. Build the ML schemas in the test with clone(rosterFormSchemaFixture): set data.meta.enabledLocales to ['en','pl'] and switch the type of `name` and/or `slug` to 'mltext' per case (the slug keeps preset field name, type slug). Use a 422 POST reply so the create form stays mounted for the body assertion. Run them; they must fail.
|
||||
|
||||
GREEN in admin/src/components/form/formState.ts:
|
||||
- Export changedLocales(previous: unknown, next: unknown): string[] built on localeRecord: the locales present on either side whose text differs, a missing key counting as ''.
|
||||
- Export a type PresetEdited = (target: string, locale?: string) => boolean and a function presetUpdates(fields: FormField[], source: string, previous: unknown, next: unknown, values: AdminRecord, activeLocale: string, isEdited: PresetEdited): AdminRecord returning target name -> new value for the preset targets of `source` to rewrite. Targets are fields whose preset.field equals source and whose type is 'text' or 'mltext'; every other type is skipped. The source counts as ML when its field (looked up in fields) has type mltext or mlmarkdown. Rules, all through the existing presetValue so slug/exact stay one implementation: text -> text and ML -> text as in the behavior block (ML reads localeRecord(next)[activeLocale]); text -> ML writes only the active locale onto a copy of localeRecord(values[target]); ML -> ML copies localeRecord(values[target]) and rewrites each locale from changedLocales(previous, next) that isEdited(target, locale) does not report, including the target only when a locale was written. Write a doc comment stating the per-locale rule and that a preset never writes a locale the administrator neither typed in nor sees.
|
||||
|
||||
GREEN in admin/src/views/FormView.vue:
|
||||
- Add editedLocales (Map of field name to Set of locales) next to `edited`, and a ref mlLocale, set in load() after the schema arrives to the first of schemaEnabledLocales(schema.meta) or 'en' (the same default MLTextField uses).
|
||||
- Follow ML_LOCALE_CHANGE (import it from formState): register a window listener in the existing onMounted and remove it in the existing onBeforeUnmount; a non-empty string detail becomes mlLocale. Add a watch on activeTab that resets mlLocale to the first enabled locale, because FormGrid remounts the new panel's ML controls on that locale.
|
||||
- In update(): read previous = values.value[name] before building next; keep edited.add(name); when the field named `name` is mltext or mlmarkdown, add every changedLocales(previous, value) locale to editedLocales for that name. On create, replace the text-only loop by merging presetUpdates(fields.value, name, previous, value, values.value, mlLocale.value, isEdited) into next, where isEdited checks `edited` when no locale is given and editedLocales otherwise. Update the comment above `edited` to describe text and mltext presets and the per-locale stop. Update mode stays untouched.
|
||||
|
||||
Run the targeted and full Vitest suites and the typecheck. Then run npm --prefix admin run build and stage modules/boardwalk/dist with the SPA change. Commit: feat(admin): follow presets on mltext fields per locale (SPA, tests, dist; no .planning files, no co-author trailers).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && npm --prefix admin test -- tests/form/formState.test.ts tests/form/FormView.test.ts tests/form/MLFields.test.ts && npm --prefix admin test && npm --prefix admin run typecheck && scripts/check-admin-dist.sh</automated>
|
||||
</verify>
|
||||
<done>All behavior cases pass in formState.test.ts and FormView.test.ts; the existing text preset tests and the ML field tests stay green; vue-tsc is clean; check-admin-dist reports dist matches a fresh build; one commit holding the SPA change, its tests and modules/boardwalk/dist.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Document preset on mltext fields, regenerate OpenAPI, run the full suite</name>
|
||||
<files>modules/cabana/schema_types.go, modules/cabana/admin_openapi.go, admin/openapi/admin.json, admin/src/api/schema.d.ts, modules/cabana/README.md, docs/backend/forms.md</files>
|
||||
<read_first>docs/backend/forms.md (Field options ~line 106-124, Markdown and multilingual fields ~line 326-340), modules/cabana/README.md (line 28 and the FieldPreset row ~line 225), modules/cabana/schema_types.go (Preset field and FieldPreset comments ~lines 312-332), modules/cabana/admin_openapi.go (form schema description ~line 285)</read_first>
|
||||
<action>
|
||||
Comments and annotation: in schema_types.go say the Preset field and FieldPreset apply to a text or mltext field and name a text or mltext source; an mltext pair follows per locale, a mixed pair follows the active locale. In admin_openapi.go change the form-schema description sentence to "`preset` on a text or mltext field names the text or mltext field it follows on the create form (type slug or exact; an mltext pair follows per locale) until the administrator edits it." Then run scripts/check-admin-openapi.sh (no args) to regenerate admin/openapi/admin.json and admin/src/api/schema.d.ts; never hand-edit them.
|
||||
|
||||
modules/cabana/README.md: line 28 bullet becomes `preset` on a `type: text` or `type: mltext` field, following another text or mltext field of the same form, per locale for an mltext pair; the `cabana.FieldPreset` row says a text or mltext field's preset.
|
||||
|
||||
docs/backend/forms.md: the Field options list says `preset` on text and mltext fields; the preset paragraph says a text or mltext field follows another text or mltext field; after the existing rules sentence add the multilingual rules (an mltext target follows an mltext source per locale, and a hand edit of one locale stops only that locale; an mltext source drives a text target from the active locale; a text source fills only the active locale of an mltext target, so locales nobody looked at stay empty); the start-up sentence says "a source that is not a text or mltext field of the same form" and keeps the settings/relation-form refusal. Add one short sentence in the Markdown and multilingual fields section pointing to the preset rules with a link to the Field options anchor (check the real anchor id in the page). Keep the example neutral (acme, blog); never name a consuming application. Every identifier named must exist (TestDocsTree checks).
|
||||
|
||||
Run the drift and docs gates, then go vet ./... and go test ./... from the repo root. Commit: docs(cabana): document preset on mltext fields (schema_types.go, admin_openapi.go, admin.json, schema.d.ts, README, forms.md; no .planning files, no co-author trailers). Record in the SUMMARY the plugin YAML snippet from the context block verbatim, the discretion choices, and that sm-journal-plugin was not edited.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && bash -c 'test -z "$(gofmt -l modules/cabana)"' && scripts/check-admin-openapi.sh --check && go test ./cmd/summer -run TestDocsTree -count=1 && grep -n 'preset' modules/cabana/README.md | grep -q 'mltext' && go vet ./... && go test ./...</automated>
|
||||
</verify>
|
||||
<done>README, forms.md, the swag description and the Go doc comments describe preset on text and mltext fields with the per-locale rules; admin.json and schema.d.ts match a fresh generation; TestDocsTree, go vet ./... and go test ./... are green; one docs commit; the SUMMARY carries the sm-journal-plugin YAML snippet.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| plugin fields.yaml -> cabana schema compiler | Plugin-authored YAML is compiled at boot; a bad preset must stop start-up, not reach the SPA |
|
||||
| admin SPA -> cabana save routes | The create body now carries preset-derived locale maps; the server validates them as any other admin input |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-quick-261006-t5r-01 | Tampering | modules/cabana/form_schema.go compilePresetKey/checkPresets | low | mitigate | The widened rule admits exactly text and mltext targets and sources; TestPresetML pins the refusals of mlmarkdown/markdown targets and an mlmarkdown source, and settings/relation forms still refuse preset |
|
||||
| T-quick-261006-t5r-02 | Tampering | admin/src/views/FormView.vue preset follow | low | accept | Preset output is client convenience only; the server stores what it is sent and runs the model's Rules and required checks on the save as before (no server-side fill) |
|
||||
| T-quick-261006-t5r-03 | Information disclosure | text -> mltext preset | low | mitigate | presetUpdates writes only the active locale, so no locale the administrator never saw is silently saved; covered by the text -> ML unit and FormView cases |
|
||||
| T-quick-261006-t5r-SC | Tampering | npm/go installs | low | accept | No new npm or Go dependency; the work uses the committed lockfile, the existing Vitest setup and the pinned swag v1.16.6 run in check-admin-openapi.sh |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- go test ./modules/cabana -run 'TestPresetML|TestPresetSchema|TestPhase121BootErrors' (testcontainers needs Docker)
|
||||
- npm --prefix admin test and npm --prefix admin run typecheck
|
||||
- scripts/check-admin-dist.sh and scripts/check-admin-openapi.sh --check
|
||||
- go test ./cmd/summer -run TestDocsTree
|
||||
- go vet ./... and go test ./... from the repo root
|
||||
- git log shows three code commits (feat(cabana), feat(admin) with dist, docs(cabana)) without co-author trailers and without .planning files; nothing under ../sm-journal-plugin changed
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- A plugin can declare `preset` on an mltext slug with an mltext (or text) source and the app boots.
|
||||
- On the create form the mltext slug follows the mltext title per locale until the administrator edits that locale; mixed text/mltext pairs follow the active locale; update forms never follow.
|
||||
- Existing text presets behave exactly as before.
|
||||
- Embedded dist, OpenAPI document and TS types are in sync; docs and README describe the new rules; go vet ./... and go test ./... are green.
|
||||
- The SUMMARY records the exact sm-journal-plugin YAML (post slug preset field title, category slug preset field name) for application in that repo.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/quick/261006-t5r-make-spa-slug-preset-follow-mltext-sourc/261006-t5r-SUMMARY.md` when done
|
||||
</output>
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
phase: quick-261006-t5r
|
||||
plan: 01
|
||||
subsystem: cabana forms / admin SPA
|
||||
tags: [preset, mltext, slug, admin-spa, forms]
|
||||
status: complete
|
||||
requires: []
|
||||
provides:
|
||||
- "fields.yaml preset on text and mltext targets with text or mltext sources"
|
||||
- "SPA preset follow per locale (formState.presetUpdates, changedLocales)"
|
||||
affects:
|
||||
- sm-journal-plugin (post and category slug presets; YAML snippet below, not applied here)
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Pure preset-follow rules in formState.ts; FormView only tracks edits and the active ML locale"
|
||||
key-files:
|
||||
created:
|
||||
- modules/cabana/preset_ml_test.go
|
||||
modified:
|
||||
- modules/cabana/form_schema.go
|
||||
- modules/cabana/phase121_form_test.go
|
||||
- modules/cabana/phase121_schema_boot_test.go
|
||||
- admin/src/components/form/formState.ts
|
||||
- admin/src/views/FormView.vue
|
||||
- admin/tests/form/formState.test.ts
|
||||
- admin/tests/form/FormView.test.ts
|
||||
- modules/boardwalk/dist/index.html
|
||||
- modules/boardwalk/dist/assets/index-DPCIeFlT.js
|
||||
- modules/cabana/schema_types.go
|
||||
- modules/cabana/admin_openapi.go
|
||||
- admin/openapi/admin.json
|
||||
- admin/src/api/schema.d.ts
|
||||
- modules/cabana/README.md
|
||||
- docs/backend/forms.md
|
||||
decisions:
|
||||
- "ML -> ML presets follow per locale: only locales whose source text changed are rewritten, each from its own locale"
|
||||
- "Hand edits of an ML preset target are tracked per locale; editing locale L stops only L"
|
||||
- "A preset never writes a locale the administrator neither typed in nor sees: text -> mltext writes only the active locale"
|
||||
- "mltext -> text follows the source's active-locale string (FormView tracks the active ML locale via ML_LOCALE_CHANGE)"
|
||||
- "mlmarkdown stays out: server admits only text/mltext targets and sources"
|
||||
metrics:
|
||||
duration: "~13 min"
|
||||
completed: 2026-10-06
|
||||
estimate:
|
||||
tokens: 70000
|
||||
tasks: 3
|
||||
actuals:
|
||||
tokens: 13500
|
||||
tasks: 3
|
||||
commits: 3
|
||||
plan_head_before: 7e2b1a909bb24eae22662a9edea23fec38cc0cd1
|
||||
plan_head_after: 593217214809f8b8f45a7c2d2ea9ffd311ff2bfe
|
||||
---
|
||||
|
||||
# Quick 261006-t5r: Make SPA slug preset follow mltext source Summary
|
||||
|
||||
fields.yaml `preset` now works on `mltext` fields: cabana accepts text/mltext targets and sources, and the admin SPA follows an mltext title into an mltext slug per locale, stopping only the locale the administrator edits by hand.
|
||||
|
||||
## What changed
|
||||
|
||||
**Task 1 - cabana (7afad73, `feat(cabana): accept preset on mltext fields`)**
|
||||
- `compilePresetKey` admits `type: text` or `type: mltext`; anything else fails with `preset is only valid on type: text or mltext`.
|
||||
- `checkPresets` admits a text or mltext source; otherwise `field %s: preset field %s must be a text or mltext field`.
|
||||
- New DB-free `TestPresetML` (RED confirmed before the change) covers mltext->mltext (mapping, short form, exact), text->mltext, mltext->text, and the refusals (mlmarkdown and markdown targets, mlmarkdown source). The Journal-shaped field serializes with `"type":"mltext"` and `"preset":{"field":"title","type":"slug"}`.
|
||||
- `TestPresetSchema` / `TestPhase121BootErrors` rows updated to the new messages (testcontainers Postgres, green). Settings and relation forms still refuse `preset`.
|
||||
|
||||
**Task 2 - admin SPA (b5d8506, `feat(admin): follow presets on mltext fields per locale`)**
|
||||
- `formState.ts`: `changedLocales(previous, next)` and `presetUpdates(fields, source, previous, next, values, activeLocale, isEdited)` (plus `PresetEdited` type) hold every preset-follow rule; all values still go through `presetValue`, so slug/exact remain one implementation.
|
||||
- `FormView.vue`: `update()` reads the previous value, records per-locale hand edits of ML fields in `editedLocales`, and on create merges `presetUpdates` into the next values. `mlLocale` follows the window `ML_LOCALE_CHANGE` event, starts on the first enabled locale after load, and resets to it when `activeTab` changes. Update mode unchanged.
|
||||
- Tests: 6 new formState unit cases, 4 new FormView cases (Journal ML->ML per-locale with POST body check, update mode, mltext->text, text->mltext). The existing text preset test is unchanged and green. Full Vitest: 1064 passed; vue-tsc clean.
|
||||
- `modules/boardwalk/dist` rebuilt (old `index-7UEAlsFO.js` removed, `index-DPCIeFlT.js` added; intentional hashed-bundle swap); `check-admin-dist.sh` reports a match.
|
||||
|
||||
**Task 3 - docs (5932172, `docs(cabana): document preset on mltext fields`)**
|
||||
- `schema_types.go` Preset/FieldPreset comments and the form-schema swag description in `admin_openapi.go` name text or mltext; `admin.json` and `schema.d.ts` regenerated with `scripts/check-admin-openapi.sh` (the `--check` gate passes).
|
||||
- `modules/cabana/README.md` (Features bullet, `cabana.FieldPreset` row) and `docs/backend/forms.md` (Field options list, preset paragraph, new multilingual-rules paragraph, start-up refusal sentence, and a pointer from the Markdown and multilingual fields section to `#field-options`).
|
||||
- `TestDocsTree`, `go vet ./...` and `go test ./...` green.
|
||||
|
||||
## Discretion choices (from the plan)
|
||||
|
||||
- ML -> ML follows per locale: only the locales whose source text changed in this update are rewritten, each from its own locale.
|
||||
- Edits of an ML target are tracked per locale: a hand edit of locale L stops L only.
|
||||
- A preset never writes a locale the administrator neither typed in (source side) nor is looking at (target side): text -> mltext writes only the active locale; untouched locales stay empty so the server's translation fallback applies, and no unseen duplicate translations get saved.
|
||||
- mltext -> text follows the source's active-locale string (FormView tracks the active ML locale).
|
||||
- mlmarkdown stays out: the server admits text/mltext targets and text/mltext sources only.
|
||||
|
||||
## Plugin follow-up (sm-journal-plugin, NOT edited in this task)
|
||||
|
||||
sm-journal-plugin was not edited. Apply in that repo:
|
||||
|
||||
models/post/fields.yaml, the slug field gains a preset:
|
||||
|
||||
```yaml
|
||||
slug:
|
||||
label: golem15.journal::lang.post.slug
|
||||
type: mltext
|
||||
span: right
|
||||
tab: golem15.journal::lang.post.tab_edit
|
||||
required: true
|
||||
preset:
|
||||
field: title
|
||||
type: slug
|
||||
```
|
||||
|
||||
models/category/fields.yaml, the slug field gains `preset: { field: name, type: slug }` the same way (its source `name` is mltext). models/tag/fields.yaml already has a text -> text preset and needs nothing.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written. (Process note: the formState helpers were written just before their tests; RED was then demonstrated by running the new tests against the committed `formState.ts`/`FormView.vue` — 9 failures — before restoring the implementation.)
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None. No new routes or trust-boundary surface; the server-side widening admits exactly text and mltext (pinned by TestPresetML), and preset output remains client convenience validated by the normal save path.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: modules/cabana/preset_ml_test.go
|
||||
- FOUND: commits 7afad73, b5d8506, 5932172 (no co-author trailers, no .planning files)
|
||||
- Gates: check-admin-dist, check-admin-openapi --check, TestDocsTree, go vet ./..., go test ./..., Vitest full suite, vue-tsc
|
||||
Reference in New Issue
Block a user