Files
summercms-initial-research/.planning/phases/07-admin-forms-lists/07-01-PLAN.md
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

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
07-admin-forms-lists 01 execute 1
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
true
truths artifacts key_links
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)
path provides exports
summercms/src/admin/yaml/FieldsYamlSchema.scala FormFieldConfig, TriggerConfig, TabsConfig ADTs with circe decoders
FormFieldConfig
FieldsYamlConfig
path provides exports
summercms/src/admin/yaml/ColumnsYamlSchema.scala ListColumnConfig ADT with circe decoders
ListColumnConfig
ColumnsYamlConfig
path provides exports
summercms/src/admin/yaml/YamlParser.scala ZIO-wrapped YAML parsing service
YamlParser
path provides exports
summercms/src/admin/forms/WidgetRegistry.scala Widget type to renderer mapping
WidgetRegistry
FormWidget
path provides exports
summercms/src/admin/forms/FormRenderer.scala Form container HTML generation
FormRenderer
from to via pattern
summercms/src/admin/yaml/YamlParser.scala summercms/src/admin/yaml/FieldsYamlSchema.scala circe-yaml parsing parser.parse.*as[FieldsYamlConfig]
from to via pattern
summercms/src/admin/forms/FormRenderer.scala summercms/src/admin/forms/WidgetRegistry.scala widget resolution for field rendering registry.resolve
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.

<execution_context> @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md </execution_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 Task 1: Add dependencies and create YAML schema ADTs summercms/build.mill summercms/src/admin/yaml/FieldsYamlSchema.scala summercms/src/admin/yaml/ColumnsYamlSchema.scala 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. Run mill summercms.compile - should compile without errors. Check that all case classes have proper circe decoders by reviewing generated code structure. Build compiles with new dependencies. YAML schema ADTs defined with all WinterCMS-compatible fields and circe decoders.

Task 2: Create YamlParser service and Widget registry summercms/src/admin/yaml/YamlParser.scala summercms/src/admin/forms/WidgetRegistry.scala summercms/src/admin/forms/FormField.scala summercms/src/admin/forms/FormContext.scala 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:
    def render(field: FormField, name: String, value: Any, ctx: FormContext): scalatags.Text.Frag
    
  • WidgetRegistry as ZIO service with Ref-based mutable registry:
    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 Run mill summercms.compile - should compile without errors. Write a simple test parsing a fields.yaml string to verify parsing works. YamlParser service parses YAML strings/files into typed configs. WidgetRegistry provides type-to-widget mapping with registration and resolution. FormContext provides form state access.
Task 3: Create FormRenderer with tab and section support summercms/src/admin/forms/FormRenderer.scala Create FormRenderer.scala as a ZIO service that generates form HTML:
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:

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:

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:

field.trigger.map(t => Seq(
  data("trigger-field") := t.field,
  data("trigger-action") := t.action,
  data("trigger-condition") := t.condition
))
Run `mill summercms.compile` - should compile without errors. Manually verify FormRenderer produces valid HTML structure by calling render with sample config. FormRenderer generates complete form HTML from FieldsYamlConfig. Supports tabs, sections, field grouping, error display, and HTMX submission. Integrates with WidgetRegistry for field rendering. After completing all tasks:
  1. Compile check:

    mill summercms.compile
    

    Must succeed with no errors.

  2. YAML parsing verification: Create a test that parses sample fields.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

<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>
After completion, create `.planning/phases/07-admin-forms-lists/07-01-SUMMARY.md`