155 lines
16 KiB
Markdown
155 lines
16 KiB
Markdown
# Phase 12.2: Admin form fields: date, file upload, relation editing with deferred binding - Context
|
|
|
|
**Gathered:** 2026-10-02
|
|
**Status:** Ready for planning
|
|
|
|
<domain>
|
|
## Phase Boundary
|
|
|
|
Plugin admin forms gain three things a downstream project on SummerCMS v0.1 needed:
|
|
|
|
1. A `type: datepicker` field (date, datetime, time).
|
|
2. A `type: fileupload` field for `attachOne` / `attachMany`.
|
|
3. Full child CRUD in the relation manager: create, update and delete of `hasMany` and `belongsToMany` related records in a modal, alongside the existing link and unlink.
|
|
|
|
All three use Winter-like deferred binding, so they also work on a record that has not been saved yet. Work happens in `summercms.go` only: `modules/cabana`, `modules/lagoon` and `modules/lagoon/attach`, and the admin SPA (`admin/`). The module READMEs and the `docs/` pages are updated in the same change. The phase ships as tag v0.1.1. It is urgent and independent of Phases 12.1 and 13.
|
|
|
|
Out of scope: image cropping, nested relation managers inside a modal, Winter field types other than these three, and changes to fonoteka.go's hand-rolled upload code.
|
|
|
|
</domain>
|
|
|
|
<decisions>
|
|
## Implementation Decisions
|
|
|
|
### Deferred binding
|
|
- **D-01:** Pending bindings live in Winter's `deferred_bindings` table, with the same columns as Winter's two migrations: `id`, `master_type`, `master_field`, `slave_type`, `slave_id`, `session_key`, `pivot_data`, `is_bind`, `created_at`, `updated_at`. One column is added: the owning backend admin id. `master_type` and `slave_type` hold the same morph type string that `system_files.attachment_type` already uses. No PHP class names. The migration ships as a framework migration set in `lagoon`, beside `attach.Migrations`. — **Reversibility:** costly — the table is migrated in every host application, and changing its shape later needs a migration plus data rewrite.
|
|
- **D-02:** Session keys come from the SPA. It generates a cryptographically random key of at least 128 bits each time a form opens, and sends it with every upload, file removal, child write and the final save. The server stores the authenticated admin's id on each binding. A key used by a different admin is treated as unknown: its bindings are neither read nor committed. The server validates the key's format and length.
|
|
- **D-03:** The Winter split decides when deferral applies:
|
|
- **File uploads and file removals** are always deferred until the parent's Save, on new and saved records alike, as Winter's FileUpload widget does (`add($file, $sessionKey)`). Cancel or navigating away discards them.
|
|
- **Relation-manager child changes** (create, update, delete, link, unlink, pivot edits) are immediate on a saved parent and deferred only while the parent is unsaved, as Winter's RelationController does.
|
|
- **D-04:** Commit and discard work like this:
|
|
- On the parent's create or update save, every binding for `(session_key, admin)` is applied inside the save transaction, after `FormBeforeCreate` / `FormBeforeUpdate` and before commit. Binds attach or link, unbinds detach or unlink or delete.
|
|
- If the transaction rolls back, the bindings stay in place, so a 422 doesn't lose the uploads.
|
|
- A successful save deletes the applied binding rows.
|
|
- **D-05:** Purging is a daily River periodic job plus `summer deferred:purge [--days=N]`. The default age is 5 days (Winter's `DeferredBinding::cleanUp(5)`), configurable. Both remove expired bindings and the orphaned slave records they point at. A deferred-created child row or an unattached `system_files` row is deleted. For files, the blob and its thumbnails are deleted after commit through the existing two-phase `attach.DeleteKeys` path.
|
|
|
|
### File upload
|
|
- **D-06:** Models declare attachments through an interface, `AttachRelations() []attach.Relation`, where each entry carries at least `Name`, `Many` (attachOne vs attachMany) and `Public`. This follows the `AdminRelationContracts()` style. A `type: fileupload` field must name a declared relation, otherwise boot fails with an error naming the plugin, controller and file.
|
|
- **D-07:** A new exported API in `lagoon/attach` stores an upload. It writes the blob under the Winter partition key and the `system_files` row, enforces MIME and size limits, applies the image-content guard (including webp, P12 D-24), and handles `sort_order` and thumbnails. `cabana` uses it, and app plugins can adopt it later. Existing app code that builds `attach.File{}` by hand (fonoteka.go album photos, collection media, user avatar) is not changed in this phase, so the change is non-breaking.
|
|
- **D-08:** Accepted `fileupload` keys: `mode` (image|file), `fileTypes`, `mimeTypes`, `maxFilesize`, `maxFiles`, `imageWidth`, `imageHeight`, `thumbOptions` (the thumb mode), `useCaption` (edit title and description) and `prompt`. Any other key is a boot error (P9 D-06). No cropping. Limits are enforced on the server, never only in the SPA, and the upload route has a `MaxBytesReader` cap (P7 D-04 pattern).
|
|
- **D-09:** Supported operations: upload (deferred, D-03), image preview and thumbnail, remove (deferred), reorder for `attachMany` (`sort_order`), and title/description editing when `useCaption` is set. They work on both saved and unsaved records.
|
|
- **D-10:** Both public and protected attachments are supported. A relation with `Public: true` stores `is_public=true` and uses the existing Winter-shaped public URLs (P12 D-22). A relation with `Public: false` stores `is_public=false`, and the SPA gets its download and thumbnail through an authenticated admin API route under the `backend` guard. That route is scoped to a parent record the admin may access (or to the admin's own pending bindings).
|
|
|
|
### Relation child CRUD
|
|
- **D-11:** The child modal's form comes from `config_relation.yaml` `manage.form` (Winter style, e.g. `$/vendor/plugin/models/child/fields.yaml`), with an optional `view.form`. It is compiled at boot by the same typed form-schema pipeline and with the same fail-loud rules.
|
|
- **D-12:** `toolbarButtons` follow Winter: `create`, `update`, `delete`, `link`, `unlink`. On `hasMany`, `delete` deletes the child row through the model so lifecycle hooks and soft delete fire, and `unlink` sets its foreign key to null. On `belongsToMany`, `delete` deletes the related record and `unlink` removes the pivot row (as today). Unknown buttons fail at boot.
|
|
- **D-13:** `RelationContract` (`modules/cabana/relation.go`) is extended to describe `hasMany` (relation kind plus the child's foreign key) next to the existing pivot shape. The framework still never guesses table or column names (P9 D-16).
|
|
- **D-14:** Winter's `pivot.form` is supported for `belongsToMany`. Pivot fields are edited in a modal when linking and later through "edit pivot". Pivot input is filled through a whitelist of the pivot form's fields. `RelationBeforeLink` still stamps the server-owned pivot columns, and those columns can't be set from the request.
|
|
- **D-15:** Every child endpoint is scoped to its parent. The parent is loaded through `FormExtendQuery` with the controller's permissions, and the child must belong to that parent: by FK for `hasMany`, by pivot row for `belongsToMany`, or by the admin's session-key bindings while the parent is unsaved. Otherwise the endpoint returns `not_found`, so a child of another parent can be neither read nor changed. This is success criterion 3, and it gets security tests.
|
|
- **D-16:** Child saves go through the related model's `Fill` / `Validate` (422 envelope, P9 D-10) and its lifecycle hooks. The admin-controller hook set from P9 D-13 gains optional relation hooks for child create, update and delete in the same type-asserted style. Exact names are the planner's choice.
|
|
- **D-17:** The child modal is a full form. `datepicker` and `fileupload` work inside it with the child form's own session key, which is committed with the child's save. If the parent is unsaved too, the child itself is deferred against the parent's key. A `relation-manager` field inside a child form is a boot error.
|
|
|
|
### Datepicker
|
|
- **D-18:** For `mode: datetime`, the value is stored as `timestamptz` in UTC, and the SPA shows and edits it in the admin's browser timezone. `ignoreTimezone: true` keeps the wall-clock value unchanged, with no conversion. `date` and `time` modes never convert.
|
|
- **D-19:** Go types:
|
|
- `datetime` maps to `time.Time` / `*time.Time`.
|
|
- `date` maps to a framework `lagoon.Date` (a `DATE` column; JSON `"2026-10-02"`).
|
|
- `time` maps to `lagoon.TimeOfDay` (a `TIME` column; JSON `"14:30:00"`).
|
|
|
|
Nullable variants are included. All of them implement `Scanner` / `Valuer` and JSON marshalling. A plugin never defines its own date types (success criterion 1). Boot fails if a datepicker field's mode doesn't match its model field's Go type.
|
|
- **D-20:** Accepted `datepicker` keys: `mode`, `format` (display only; Winter/moment tokens are mapped to the SPA formatter), `minDate`, `maxDate`, `yearRange`, `firstDay`, `twelveHour` and `ignoreTimezone`. Any other key is a boot error. `minDate` / `maxDate` are also validated on the server.
|
|
- **D-21:** The SPA picker is built on Reka UI DatePicker / Calendar primitives (already in the stack, P10 D-07), styled with Direction C tokens, locale-aware, with keyboard and a11y support. Native `<input type=date>` is not used.
|
|
|
|
### Claude's Discretion
|
|
- The names of the new `cabana` routes (upload, file update/reorder/remove, child CRUD, pivot edit, protected file download), provided they sit under the existing `{prefix}/api/v1/{vendor}/{plugin}/{controller}/...` scheme, use `requireAjax` on writes, carry swag annotations, and update the admin OpenAPI document and generated TS types (P10 D-15).
|
|
- How the session key travels: a header or a body field.
|
|
- The exact shape of `attach.Relation`, the attach store API, and the `lagoon.Date` / `lagoon.TimeOfDay` method sets.
|
|
- Thumbnail size for previews (derived from `imageWidth` / `imageHeight`, with a sensible default).
|
|
- Whether list columns gain `type: date` / `type: time` renderers. Add them only if they are trivial next to the existing `type: datetime`.
|
|
- Configuration key names for the purge age and job schedule.
|
|
- Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan". Run the security-review agent, because the phase touches authorization scoping and file uploads.
|
|
|
|
</decisions>
|
|
|
|
<canonical_refs>
|
|
## Canonical References
|
|
|
|
**Downstream agents MUST read these before planning or implementing.**
|
|
|
|
### Roadmap and prior decisions
|
|
- `.planning/ROADMAP.md` § "Phase 12.2" — goal and the five success criteria
|
|
- `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md` — admin API envelope, typed schema with `DisallowUnknownField` (D-06), hooks (D-13), relation manager link/unlink (D-15, D-16)
|
|
- `.planning/phases/10-admin-vue-spa/10-CONTEXT.md` — SPA stack, FieldRenderer registry (D-05), relation save shape (D-18), cookie auth and CSRF header (D-19), admin OpenAPI → TS types (D-15)
|
|
- `.planning/phases/10.1-runtime-admin-extension-point/10.1-CONTEXT.md` — widget/partial extension seam the new field types sit next to
|
|
- `.planning/phases/12-p-ytarium-api-collections-and-albums/12-CONTEXT.md` — attach URL prefix (D-22), webp image guard (D-24)
|
|
|
|
### Winter reference behaviour (in the meta repo, `../examples/golem15-wintercms-starter`)
|
|
- `vendor/winter/storm/src/Database/Migrations/2013_10_01_000001_Db_Deferred_Bindings.php` and `2021_01_19_000001_Db_Add_Pivot_Data_To_Deferred_Bindings.php` — table shape (D-01)
|
|
- `vendor/winter/storm/src/Database/Models/DeferredBinding.php` — `cleanUp(int $days = 5)` purge semantics (D-05)
|
|
- `vendor/winter/storm/src/Database/Traits/DeferredBinding.php` — commit-on-save semantics (D-04)
|
|
- `modules/backend/formwidgets/FileUpload.php` — upload/remove/sort/caption behaviour and config keys (D-08, D-09)
|
|
- `modules/backend/formwidgets/DatePicker.php` — modes, keys, timezone handling (D-18, D-20)
|
|
- `modules/backend/behaviors/RelationController.php` — toolbar buttons, `manage.form`, `pivot.form`, deferral only on unsaved parents (D-03, D-11, D-12, D-14)
|
|
|
|
### Framework code and docs to update
|
|
- `modules/cabana/README.md`, `modules/lagoon/README.md` (covers `attach`) — must document new API, keys and commands in the same change
|
|
- `docs/backend/forms.md`, `docs/backend/relation-manager.md`, `docs/database/attachments.md`, `docs/database/models.md` — affected docs pages; `go test ./cmd/summer -run TestDocsTree` and `summer docs:build --check` must pass
|
|
|
|
</canonical_refs>
|
|
|
|
<code_context>
|
|
## Existing Code Insights
|
|
|
|
### Reusable Assets
|
|
- `modules/lagoon/attach`: `attach.File` (`system_files`), `BlobKey` / `PartitionDirectory`, `PublicURL`, `File.Thumb`, `StaticHandlerPublic` (is_public gate), and the two-phase `DeleteForOwner` / `DeleteKeys`. There is no "store an upload" helper yet. D-07 adds one.
|
|
- `modules/cabana/relation.go`: `RelationContract`, `RelationSchema`, `RelationService`, link/unlink mutation. Pivot-only today, so it needs `hasMany` (D-13).
|
|
- `modules/cabana/schema_types.go`: `FormField`, typed schema structs, and the boot-time compile with fail-loud errors.
|
|
- `modules/cabana/model_fields.go`: reflection over model fields. It already treats `time.Time` and Scanner/Valuer types as scalars, so the D-19 types slot in.
|
|
- `admin/src/components/form/registry.ts` + `fields/*.vue`: FieldRenderer registry. New `DatepickerField` and `FileuploadField` register here.
|
|
- `admin/src/components/relation/RelationManager.vue`, `RelationPickerModal.vue`: extend them with the create/update/delete modal and the pivot modal.
|
|
- fonoteka.go's `album_photos_controller.go` / `collection_media_controller.go` / `classes/album_files.go`: working app-side examples of building `attach.File` rows, sort order and blob writes. They are reference only and are not changed.
|
|
|
|
### Established Patterns
|
|
- Optional capabilities are interfaces that the framework type-asserts, as with `Has*` in pact and the admin hooks (P9 D-13).
|
|
- Every write route uses `requireAjax`. Every route is under the `backend` guard and enforces the controller's `RequiredPermissions`.
|
|
- Six-segment GET routes go through `nestedGet` dispatch because ServeMux pattern conflicts. New nested routes must fit this scheme (`modules/cabana/http.go`).
|
|
- Framework migrations are gormigrate sets run before plugin sets (`attach.Migrations`, `lagoon.BackendAdminMigrations`, `lagoon.QueueMigrations`).
|
|
- River periodic jobs and `summer_jobs` records come from Phase 11.
|
|
|
|
### Integration Points
|
|
- cabana `create` / `update` save transaction: apply deferred bindings there (D-04).
|
|
- `lagoon.Migrate`: register the `deferred_bindings` migration set.
|
|
- The admin OpenAPI document and the generated TS types (P10 D-15), with their drift checks.
|
|
- The committed SPA `dist/` and its drift check (P10 D-04): rebuild after the SPA changes.
|
|
|
|
</code_context>
|
|
|
|
<specifics>
|
|
## Specific Ideas
|
|
|
|
- Triggered by a downstream project on SummerCMS v0.1 that marked these three gaps with `TODO: requires SummerCMS change`. After the phase lands, tag v0.1.1 and tell the user those TODOs can be resolved.
|
|
- The user chose Winter-like deferred binding over "save the parent first" when the phase was inserted.
|
|
- Behaviour should feel like Winter's FileUpload, DatePicker and RelationController, so a WinterCMS developer recognises the YAML keys.
|
|
|
|
</specifics>
|
|
|
|
<deferred>
|
|
## Deferred Ideas
|
|
|
|
- Image cropping in fileupload (Winter's crop/resize popup): a future admin-fields phase.
|
|
- A relation manager nested inside a child modal.
|
|
- Migrating fonoteka.go's hand-rolled upload code onto the new `attach` store helper: optional follow-up in the app repo.
|
|
|
|
### Reviewed Todos (not folded)
|
|
- `2026-10-01-benchmark-the-application-on-wintercms-vs-summercms.md`: unrelated (benchmarking); only a keyword match.
|
|
- `nest-framework-packages-under-modules.md`: unrelated repo restructuring.
|
|
- `backend-admin-api-tokens.md`: admin auth, not form fields.
|
|
- `bonfire-duplicate-command-names.md`: CLI hygiene, unrelated.
|
|
|
|
</deferred>
|
|
|
|
---
|
|
|
|
*Phase: 12.2-admin-form-fields-date-file-upload-relation-editing-with-def*
|
|
*Context gathered: 2026-10-02*
|