Files
Jakub Zych 34806c1845 docs(07): create phase plan
Phase 07: Admin Forms & Lists
- 3 plan(s) in 2 wave(s)
- Wave 1: 07-01 (YAML parsing, WidgetRegistry, FormRenderer)
- Wave 2: 07-02 (form widgets), 07-03 (list rendering) - parallel
- Ready for execution
2026-02-05 14:55:56 +01:00

302 lines
11 KiB
Markdown

---
phase: 07-admin-forms-lists
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- summercms/build.mill
- summercms/src/admin/yaml/FieldsYamlSchema.scala
- summercms/src/admin/yaml/ColumnsYamlSchema.scala
- summercms/src/admin/yaml/YamlParser.scala
- summercms/src/admin/forms/FormField.scala
- summercms/src/admin/forms/WidgetRegistry.scala
- summercms/src/admin/forms/FormRenderer.scala
- summercms/src/admin/forms/FormContext.scala
autonomous: true
must_haves:
truths:
- "YAML fields.yaml content parses into typed FormFieldConfig ADT"
- "YAML columns.yaml content parses into typed ListColumnConfig ADT"
- "Widget registry resolves field type string to widget implementation"
- "Form container renders with proper structure (tabs, sections, field groups)"
artifacts:
- path: "summercms/src/admin/yaml/FieldsYamlSchema.scala"
provides: "FormFieldConfig, TriggerConfig, TabsConfig ADTs with circe decoders"
exports: ["FormFieldConfig", "FieldsYamlConfig"]
- path: "summercms/src/admin/yaml/ColumnsYamlSchema.scala"
provides: "ListColumnConfig ADT with circe decoders"
exports: ["ListColumnConfig", "ColumnsYamlConfig"]
- path: "summercms/src/admin/yaml/YamlParser.scala"
provides: "ZIO-wrapped YAML parsing service"
exports: ["YamlParser"]
- path: "summercms/src/admin/forms/WidgetRegistry.scala"
provides: "Widget type to renderer mapping"
exports: ["WidgetRegistry", "FormWidget"]
- path: "summercms/src/admin/forms/FormRenderer.scala"
provides: "Form container HTML generation"
exports: ["FormRenderer"]
key_links:
- from: "summercms/src/admin/yaml/YamlParser.scala"
to: "summercms/src/admin/yaml/FieldsYamlSchema.scala"
via: "circe-yaml parsing"
pattern: "parser\\.parse.*as\\[FieldsYamlConfig\\]"
- from: "summercms/src/admin/forms/FormRenderer.scala"
to: "summercms/src/admin/forms/WidgetRegistry.scala"
via: "widget resolution for field rendering"
pattern: "registry\\.resolve"
---
<objective>
Establish YAML parsing infrastructure and form rendering foundation for admin backend.
Purpose: This plan creates the core building blocks that all form and list functionality depends on. The YAML ADTs define the configuration language, the parser loads definitions, and the widget registry enables extensible field rendering.
Output: Typed YAML parsing for fields.yaml/columns.yaml, widget registry pattern, and basic form container rendering with ScalaTags.
</objective>
<execution_context>
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
@/home/jin/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-admin-forms-lists/07-CONTEXT.md
@.planning/phases/07-admin-forms-lists/07-RESEARCH.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Add dependencies and create YAML schema ADTs</name>
<files>
summercms/build.mill
summercms/src/admin/yaml/FieldsYamlSchema.scala
summercms/src/admin/yaml/ColumnsYamlSchema.scala
</files>
<action>
Add circe-yaml and scalatags dependencies to build.mill:
```scala
ivy"io.circe::circe-yaml:1.0.0",
ivy"io.circe::circe-generic:0.14.9",
ivy"com.lihaoyi::scalatags:0.13.1"
```
Create FieldsYamlSchema.scala with case classes matching WinterCMS fields.yaml structure:
- TriggerConfig(action: String, field: String, condition: String)
- PresetConfig(field: String, type: String)
- FormFieldConfig with all WinterCMS-compatible properties:
- type, label, span, size, tab, placeholder, comment, commentAbove
- default (as Json for flexibility), required, disabled, hidden, readOnly
- context (String or List[String] via Json), trigger, preset, dependsOn
- options (Map or method name via Json), cssClass, attributes
- Repeater-specific: form, prompt, titleFrom, minItems, maxItems, sortable
- Relation-specific: select, nameFrom, emptyOption
- FileUpload-specific: imageWidth, imageHeight
- TabsConfig for tab groupings
- FieldsYamlConfig as root (fields, tabs, secondaryTabs)
Use circe semi-auto derivation (deriveDecoder) for all types. Handle Option fields properly with default None values.
Create ColumnsYamlSchema.scala with:
- ListColumnConfig: label, type, searchable, sortable, invisible, width, relation, select, format, valueFrom, path
- FilterConfig: scope, label, conditions
- ColumnsYamlConfig as root (columns, filters, defaultSort)
Include implicit decoders in companion objects.
</action>
<verify>
Run `mill summercms.compile` - should compile without errors.
Check that all case classes have proper circe decoders by reviewing generated code structure.
</verify>
<done>
Build compiles with new dependencies. YAML schema ADTs defined with all WinterCMS-compatible fields and circe decoders.
</done>
</task>
<task type="auto">
<name>Task 2: Create YamlParser service and Widget registry</name>
<files>
summercms/src/admin/yaml/YamlParser.scala
summercms/src/admin/forms/WidgetRegistry.scala
summercms/src/admin/forms/FormField.scala
summercms/src/admin/forms/FormContext.scala
</files>
<action>
Create YamlParser.scala as a ZIO service:
```scala
trait YamlParser:
def parseFields(yaml: String): IO[YamlParseError, FieldsYamlConfig]
def parseColumns(yaml: String): IO[YamlParseError, ColumnsYamlConfig]
def parseFieldsFromFile(path: Path): IO[YamlParseError, FieldsYamlConfig]
def parseColumnsFromFile(path: Path): IO[YamlParseError, ColumnsYamlConfig]
```
- Use io.circe.yaml.parser for parsing
- Wrap errors in YamlParseError sealed trait (ParseError, FileNotFound, DecodeError)
- Live implementation using ZIO.attemptBlocking for file reads
Create FormField.scala with processed field representation:
- FormField case class with resolved values (not raw config)
- Include computed properties: effectiveLabel (from label or field name), effectiveSpan, isRequired, etc.
- Method to convert FormFieldConfig to FormField with defaults applied
Create FormContext.scala:
- FormContext case class holding: model data (Map[String, Any]), session key, form mode (create/update), validation errors
- Methods: getValue(fieldName), getError(fieldName), isCreate, isUpdate
Create WidgetRegistry.scala:
- FormWidget trait with render method signature:
```scala
def render(field: FormField, name: String, value: Any, ctx: FormContext): scalatags.Text.Frag
```
- WidgetRegistry as ZIO service with Ref-based mutable registry:
```scala
def register(typeName: String, widget: FormWidget): UIO[Unit]
def resolve(typeName: String): UIO[Option[FormWidget]]
def resolveOrDefault(typeName: String): UIO[FormWidget] // Falls back to TextWidget
```
- Include DefaultTextWidget as fallback implementation
</action>
<verify>
Run `mill summercms.compile` - should compile without errors.
Write a simple test parsing a fields.yaml string to verify parsing works.
</verify>
<done>
YamlParser service parses YAML strings/files into typed configs. WidgetRegistry provides type-to-widget mapping with registration and resolution. FormContext provides form state access.
</done>
</task>
<task type="auto">
<name>Task 3: Create FormRenderer with tab and section support</name>
<files>
summercms/src/admin/forms/FormRenderer.scala
</files>
<action>
Create FormRenderer.scala as a ZIO service that generates form HTML:
```scala
trait FormRenderer:
def render(config: FieldsYamlConfig, data: Map[String, Any], errors: Map[String, String], mode: FormMode): Task[scalatags.Text.Frag]
```
Implementation details:
1. Parse FieldsYamlConfig and group fields by tab (using field.tab property)
2. If multiple tabs exist, render horizontal tab navigation (per CONTEXT.md)
3. Within each tab, group fields by section if cssClass indicates sections
4. For each field:
- Resolve widget from WidgetRegistry
- Apply span classes (span-left, span-right, span-full based on field.span)
- Add trigger data attributes if field.trigger defined
- Render label with required asterisk if needed
- Render comment/commentAbove help text
- Call widget.render for the actual input
- Show inline error if present in errors map
Use ScalaTags for all HTML generation:
```scala
import scalatags.Text.all._
```
Form structure following CONTEXT.md decisions:
- Labels above inputs (stacked layout)
- Sections collapsible, expanded by default
- Required fields: red asterisk after label
- Help text visible below fields
- Modern minimal aesthetic via CSS classes (actual CSS in theme, not inline)
Include HTMX attributes for form submission:
```scala
form(
attr("hx-post") := submitUrl,
attr("hx-target") := "#form-container",
attr("hx-swap") := "innerHTML",
// ... fields
div(cls := "form-buttons",
button(tpe := "submit", cls := "btn btn-primary", "Save"),
button(tpe := "submit", cls := "btn btn-primary",
attr("hx-post") := s"$submitUrl?close=true",
"Save and Close")
)
)
```
Trigger support via data attributes for client-side JavaScript:
```scala
field.trigger.map(t => Seq(
data("trigger-field") := t.field,
data("trigger-action") := t.action,
data("trigger-condition") := t.condition
))
```
</action>
<verify>
Run `mill summercms.compile` - should compile without errors.
Manually verify FormRenderer produces valid HTML structure by calling render with sample config.
</verify>
<done>
FormRenderer generates complete form HTML from FieldsYamlConfig. Supports tabs, sections, field grouping, error display, and HTMX submission. Integrates with WidgetRegistry for field rendering.
</done>
</task>
</tasks>
<verification>
After completing all tasks:
1. **Compile check:**
```bash
mill summercms.compile
```
Must succeed with no errors.
2. **YAML parsing verification:**
Create a test that parses sample fields.yaml:
```yaml
fields:
title:
label: Title
span: left
required: true
content:
type: textarea
tab: Content
comment: Enter the main content
tabs:
fields:
description:
type: textarea
```
Verify it produces FieldsYamlConfig with correct field structure.
3. **Widget registry verification:**
Register a test widget and verify resolution works.
4. **Form renderer verification:**
Call FormRenderer.render with sample config and verify:
- Returns valid ScalaTags Frag
- Contains form element with hx-post attribute
- Contains field containers with proper span classes
- Required fields have asterisk in label
</verification>
<success_criteria>
- [ ] circe-yaml, circe-generic, and scalatags dependencies added to build.mill
- [ ] FieldsYamlSchema.scala defines FormFieldConfig with all WinterCMS-compatible properties
- [ ] ColumnsYamlSchema.scala defines ListColumnConfig with all properties
- [ ] YamlParser service parses YAML strings and files with proper error handling
- [ ] FormField case class provides processed field representation with defaults
- [ ] FormContext provides form state (data, errors, mode) access
- [ ] WidgetRegistry trait and live implementation with register/resolve methods
- [ ] FormRenderer generates form HTML with tabs, sections, HTMX attributes
- [ ] Trigger data attributes included for conditional visibility support
- [ ] Project compiles successfully with `mill summercms.compile`
</success_criteria>
<output>
After completion, create `.planning/phases/07-admin-forms-lists/07-01-SUMMARY.md`
</output>