docs(03): create phase plan
Phase 03: Component System - 2 plan(s) in 2 wave(s) - Wave 1: 03-01 (registration and lifecycle) - parallel - Wave 2: 03-02 (HTMX routing and response) - sequential - Ready for execution
This commit is contained in:
652
.planning/phases/03-component-system/03-01-PLAN.md
Normal file
652
.planning/phases/03-component-system/03-01-PLAN.md
Normal file
@@ -0,0 +1,652 @@
|
||||
---
|
||||
phase: 03-component-system
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- build.mill
|
||||
- summercms/src/component/SummerComponent.scala
|
||||
- summercms/src/component/ComponentDetails.scala
|
||||
- summercms/src/component/ComponentError.scala
|
||||
- summercms/src/component/PropertyDef.scala
|
||||
- summercms/src/component/PropertyValue.scala
|
||||
- summercms/src/component/ComponentSchema.scala
|
||||
- summercms/src/component/ComponentFactory.scala
|
||||
- summercms/src/component/ComponentManager.scala
|
||||
- summercms/src/plugin/PluginRegistration.scala
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Developer can define component properties in YAML with types (string, dropdown, checkbox)"
|
||||
- "Components have lifecycle hooks: init (once) and onRun (per request)"
|
||||
- "Plugins can register components via registerComponents method"
|
||||
- "ComponentManager collects all components from loaded plugins"
|
||||
- "Property schemas parse from YAML with validation rules"
|
||||
artifacts:
|
||||
- path: "summercms/src/component/SummerComponent.scala"
|
||||
provides: "Base trait for all components with lifecycle"
|
||||
contains: "trait SummerComponent"
|
||||
- path: "summercms/src/component/ComponentManager.scala"
|
||||
provides: "Service managing all registered components"
|
||||
exports: ["ComponentManager"]
|
||||
- path: "summercms/src/component/PropertyDef.scala"
|
||||
provides: "YAML property definition types"
|
||||
contains: "case class PropertyDef"
|
||||
- path: "summercms/src/component/ComponentSchema.scala"
|
||||
provides: "YAML component schema parser"
|
||||
contains: "object ComponentSchema"
|
||||
key_links:
|
||||
- from: "summercms/src/component/ComponentManager.scala"
|
||||
to: "summercms/src/plugin/PluginManager.scala"
|
||||
via: "collects components from plugin registrations"
|
||||
pattern: "PluginManager\\.getRegistration"
|
||||
- from: "summercms/src/component/ComponentSchema.scala"
|
||||
to: "circe-yaml"
|
||||
via: "YAML parsing"
|
||||
pattern: "parser\\.parse"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the component registration and lifecycle foundation for SummerCMS.
|
||||
|
||||
Purpose: Components are reusable UI pieces that plugins provide. This plan establishes the component trait, property schema parsing, and the manager that collects components from plugins. The HTMX routing (03-02) builds on this foundation.
|
||||
|
||||
Output: SummerComponent trait, PropertyDef/PropertyValue types, ComponentSchema parser, ComponentManager service, updated PluginRegistration
|
||||
</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/03-component-system/03-RESEARCH.md
|
||||
@.planning/phases/03-component-system/03-CONTEXT.md
|
||||
@build.mill
|
||||
@summercms/src/plugin/PluginManager.scala
|
||||
@summercms/src/plugin/PluginRegistration.scala
|
||||
@summercms/src/plugin/SummerPlugin.scala
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add component system dependencies to build.mill</name>
|
||||
<files>build.mill</files>
|
||||
<action>
|
||||
Add the following dependencies to build.mill mvnDeps after the existing Flyway/logging dependencies:
|
||||
|
||||
```scala
|
||||
// Component system - Templating
|
||||
mvn"io.pebbletemplates:pebble:4.1.1",
|
||||
|
||||
// Component system - HTMX integration
|
||||
mvn"dev.zio::zio-http-htmx:3.0.1",
|
||||
|
||||
// Component system - ScalaTags for programmatic HTML
|
||||
mvn"com.lihaoyi::scalatags:0.13.1",
|
||||
```
|
||||
|
||||
Note:
|
||||
- Use zio-http-htmx:3.0.1 to match zio-http:3.0.1 already in build.mill
|
||||
- htmx4s-constants is optional (adds type-safe HTMX attribute constants) - skip for now, can add later if needed
|
||||
- circe-yaml:0.16.1 will be added by Phase 2 plans - if not present yet, add it:
|
||||
|
||||
```scala
|
||||
// YAML parsing (for component.yaml and plugin.yaml)
|
||||
mvn"io.circe::circe-yaml:0.16.1",
|
||||
mvn"io.circe::circe-generic:0.14.10",
|
||||
mvn"io.circe::circe-parser:0.14.10",
|
||||
```
|
||||
</action>
|
||||
<verify>Run `./mill summercms.compile` - dependencies resolve successfully</verify>
|
||||
<done>build.mill contains Pebble 4.1.1, zio-http-htmx, ScalaTags dependencies</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create component property types and schema parser</name>
|
||||
<files>
|
||||
summercms/src/component/PropertyDef.scala
|
||||
summercms/src/component/PropertyValue.scala
|
||||
summercms/src/component/ComponentSchema.scala
|
||||
summercms/src/component/ComponentError.scala
|
||||
summercms/src/component/ComponentDetails.scala
|
||||
</files>
|
||||
<action>
|
||||
Create `summercms/src/component/` directory with these files:
|
||||
|
||||
**ComponentError.scala:**
|
||||
```scala
|
||||
package component
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/** Component system error ADT */
|
||||
enum ComponentError:
|
||||
case SchemaNotFound(path: Path)
|
||||
case SchemaParseError(path: Path, message: String)
|
||||
case ComponentNotFound(alias: String)
|
||||
case InvalidHandler(alias: String, handler: String, reason: String)
|
||||
case RenderError(alias: String, template: String, cause: Throwable)
|
||||
case PropertyValidationError(alias: String, property: String, message: String)
|
||||
case CsrfViolation(message: String)
|
||||
|
||||
def message: String = this match
|
||||
case SchemaNotFound(p) => s"Component schema not found: $p"
|
||||
case SchemaParseError(p, m) => s"Failed to parse component schema at $p: $m"
|
||||
case ComponentNotFound(a) => s"Component not found: $a"
|
||||
case InvalidHandler(a, h, r) => s"Invalid handler '$h' on component '$a': $r"
|
||||
case RenderError(a, t, c) => s"Failed to render template '$t' for component '$a': ${c.getMessage}"
|
||||
case PropertyValidationError(a, p, m) => s"Property '$p' on component '$a' failed validation: $m"
|
||||
case CsrfViolation(m) => s"CSRF violation: $m"
|
||||
```
|
||||
|
||||
**ComponentDetails.scala:**
|
||||
```scala
|
||||
package component
|
||||
|
||||
/** Component metadata from YAML schema */
|
||||
case class ComponentDetails(
|
||||
name: String,
|
||||
description: String,
|
||||
icon: Option[String] = None
|
||||
)
|
||||
```
|
||||
|
||||
**PropertyValue.scala:**
|
||||
```scala
|
||||
package component
|
||||
|
||||
import io.circe.*
|
||||
|
||||
/** Runtime property values for components */
|
||||
sealed trait PropertyValue:
|
||||
def asString: Option[String]
|
||||
def asBoolean: Option[Boolean]
|
||||
def asInt: Option[Int]
|
||||
def asList: Option[List[PropertyValue]]
|
||||
def asMap: Option[Map[String, PropertyValue]]
|
||||
|
||||
object PropertyValue:
|
||||
case class StringVal(value: String) extends PropertyValue:
|
||||
def asString = Some(value)
|
||||
def asBoolean = None
|
||||
def asInt = value.toIntOption
|
||||
def asList = None
|
||||
def asMap = None
|
||||
|
||||
case class BoolVal(value: Boolean) extends PropertyValue:
|
||||
def asString = Some(value.toString)
|
||||
def asBoolean = Some(value)
|
||||
def asInt = None
|
||||
def asList = None
|
||||
def asMap = None
|
||||
|
||||
case class IntVal(value: Int) extends PropertyValue:
|
||||
def asString = Some(value.toString)
|
||||
def asBoolean = None
|
||||
def asInt = Some(value)
|
||||
def asList = None
|
||||
def asMap = None
|
||||
|
||||
case class ListVal(values: List[PropertyValue]) extends PropertyValue:
|
||||
def asString = None
|
||||
def asBoolean = None
|
||||
def asInt = None
|
||||
def asList = Some(values)
|
||||
def asMap = None
|
||||
|
||||
case class ObjectVal(fields: Map[String, PropertyValue]) extends PropertyValue:
|
||||
def asString = None
|
||||
def asBoolean = None
|
||||
def asInt = None
|
||||
def asList = None
|
||||
def asMap = Some(fields)
|
||||
|
||||
case object NullVal extends PropertyValue:
|
||||
def asString = None
|
||||
def asBoolean = None
|
||||
def asInt = None
|
||||
def asList = None
|
||||
def asMap = None
|
||||
|
||||
/** Parse JSON value to PropertyValue */
|
||||
def fromJson(json: Json): PropertyValue =
|
||||
json.fold(
|
||||
jsonNull = NullVal,
|
||||
jsonBoolean = BoolVal(_),
|
||||
jsonNumber = n => n.toInt.map(IntVal(_)).getOrElse(StringVal(n.toString)),
|
||||
jsonString = StringVal(_),
|
||||
jsonArray = arr => ListVal(arr.map(fromJson).toList),
|
||||
jsonObject = obj => ObjectVal(obj.toMap.view.mapValues(fromJson).toMap)
|
||||
)
|
||||
|
||||
/** Implicit decoder for circe */
|
||||
given Decoder[PropertyValue] = Decoder.instance { cursor =>
|
||||
Right(fromJson(cursor.value))
|
||||
}
|
||||
```
|
||||
|
||||
**PropertyDef.scala:**
|
||||
```scala
|
||||
package component
|
||||
|
||||
import io.circe.*
|
||||
import io.circe.generic.semiauto.*
|
||||
|
||||
/** Property type enum matching WinterCMS Inspector types */
|
||||
enum PropertyType:
|
||||
case String, Text, Number, Checkbox, Dropdown, Set
|
||||
case Dictionary, Object, ObjectList, StringList
|
||||
case Autocomplete, ColorPicker, CodeEditor, FileUpload
|
||||
|
||||
object PropertyType:
|
||||
/** Parse string to PropertyType (case-insensitive) */
|
||||
def fromString(s: String): Option[PropertyType] =
|
||||
PropertyType.values.find(_.toString.equalsIgnoreCase(s))
|
||||
|
||||
given Decoder[PropertyType] = Decoder.decodeString.emap { s =>
|
||||
fromString(s).toRight(s"Unknown property type: $s")
|
||||
}
|
||||
|
||||
/** Validation rules for a property */
|
||||
case class ValidationRules(
|
||||
required: Option[Boolean] = None,
|
||||
requiredMessage: Option[String] = None,
|
||||
min: Option[Int] = None,
|
||||
minMessage: Option[String] = None,
|
||||
max: Option[Int] = None,
|
||||
maxMessage: Option[String] = None,
|
||||
pattern: Option[String] = None,
|
||||
patternMessage: Option[String] = None
|
||||
)
|
||||
|
||||
object ValidationRules:
|
||||
given Decoder[ValidationRules] = deriveDecoder[ValidationRules]
|
||||
|
||||
/** Property definition from component.yaml */
|
||||
case class PropertyDef(
|
||||
`type`: PropertyType,
|
||||
title: String,
|
||||
description: Option[String] = None,
|
||||
default: Option[PropertyValue] = None,
|
||||
options: Option[Map[String, String]] = None, // For dropdown/set
|
||||
group: Option[String] = None,
|
||||
showExternalParam: Option[Boolean] = None, // Allow {{ :param }} binding
|
||||
validation: Option[ValidationRules] = None
|
||||
):
|
||||
def propertyType: PropertyType = `type`
|
||||
def isRequired: Boolean = validation.flatMap(_.required).getOrElse(false)
|
||||
|
||||
object PropertyDef:
|
||||
given Decoder[PropertyDef] = deriveDecoder[PropertyDef]
|
||||
```
|
||||
|
||||
**ComponentSchema.scala:**
|
||||
```scala
|
||||
package component
|
||||
|
||||
import io.circe.*
|
||||
import io.circe.generic.semiauto.*
|
||||
import io.circe.yaml.parser
|
||||
import zio.*
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
/** Component schema parsed from component.yaml */
|
||||
case class ComponentSchema(
|
||||
name: String,
|
||||
description: String,
|
||||
icon: Option[String] = None,
|
||||
properties: Map[String, PropertyDef] = Map.empty
|
||||
):
|
||||
def toDetails: ComponentDetails = ComponentDetails(name, description, icon)
|
||||
|
||||
object ComponentSchema:
|
||||
given Decoder[ComponentSchema] = deriveDecoder[ComponentSchema]
|
||||
|
||||
/** Parse YAML string into ComponentSchema */
|
||||
def parse(yaml: String): Either[String, ComponentSchema] =
|
||||
parser.parse(yaml)
|
||||
.flatMap(_.as[ComponentSchema])
|
||||
.left.map(_.getMessage)
|
||||
|
||||
/** Parse YAML file at path */
|
||||
def parseFile(path: Path): IO[ComponentError, ComponentSchema] =
|
||||
for
|
||||
exists <- ZIO.attemptBlocking(Files.exists(path))
|
||||
.orDie
|
||||
_ <- ZIO.fail(ComponentError.SchemaNotFound(path)).when(!exists)
|
||||
content <- ZIO.attemptBlocking(Files.readString(path))
|
||||
.mapError(e => ComponentError.SchemaParseError(path, e.getMessage))
|
||||
schema <- ZIO.fromEither(parse(content))
|
||||
.mapError(msg => ComponentError.SchemaParseError(path, msg))
|
||||
yield schema
|
||||
```
|
||||
</action>
|
||||
<verify>Run `./mill summercms.compile` - all component types compile without errors</verify>
|
||||
<done>PropertyValue ADT, PropertyDef with validation, PropertyType enum, ComponentSchema parser, ComponentError ADT</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Create SummerComponent trait and ComponentManager service</name>
|
||||
<files>
|
||||
summercms/src/component/SummerComponent.scala
|
||||
summercms/src/component/ComponentFactory.scala
|
||||
summercms/src/component/ComponentManager.scala
|
||||
summercms/src/plugin/PluginRegistration.scala
|
||||
</files>
|
||||
<action>
|
||||
**SummerComponent.scala:**
|
||||
```scala
|
||||
package component
|
||||
|
||||
import zio.*
|
||||
import java.nio.file.Path
|
||||
|
||||
/** Page context available during component rendering */
|
||||
case class PageContext(
|
||||
url: String,
|
||||
params: Map[String, String] = Map.empty,
|
||||
isBackendEditor: Boolean = false
|
||||
):
|
||||
def param(name: String): Option[String] = params.get(name)
|
||||
|
||||
/** Base trait for all SummerCMS components */
|
||||
trait SummerComponent:
|
||||
/** Component alias (used in page templates) */
|
||||
def alias: String
|
||||
|
||||
/** Component metadata from YAML schema */
|
||||
def details: ComponentDetails
|
||||
|
||||
/** Property schema from component.yaml */
|
||||
def propertySchema: Map[String, PropertyDef]
|
||||
|
||||
/** Current property values (set from page/layout configuration) */
|
||||
def properties: Ref[Map[String, PropertyValue]]
|
||||
|
||||
/** Page context for this request */
|
||||
def pageContext: PageContext
|
||||
|
||||
/** Path to component's template directory */
|
||||
def templateDir: Path
|
||||
|
||||
/**
|
||||
* Called once when component is initialized.
|
||||
* Use for one-time setup before any handlers or rendering.
|
||||
*/
|
||||
def init: ZIO[ComponentEnv, ComponentError, Unit] = ZIO.unit
|
||||
|
||||
/**
|
||||
* Called when component is bound to page/layout, part of page lifecycle.
|
||||
* Use for loading data needed for rendering.
|
||||
*/
|
||||
def onRun: ZIO[ComponentEnv, ComponentError, Unit] = ZIO.unit
|
||||
|
||||
/**
|
||||
* Get a property value by name, with fallback to default from schema.
|
||||
*/
|
||||
def property(name: String): UIO[Option[PropertyValue]] =
|
||||
properties.get.map { props =>
|
||||
props.get(name).orElse(propertySchema.get(name).flatMap(_.default))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a property as string.
|
||||
*/
|
||||
def propertyString(name: String): UIO[Option[String]] =
|
||||
property(name).map(_.flatMap(_.asString))
|
||||
|
||||
/**
|
||||
* Get a property as int.
|
||||
*/
|
||||
def propertyInt(name: String): UIO[Option[Int]] =
|
||||
property(name).map(_.flatMap(_.asInt))
|
||||
|
||||
/**
|
||||
* Get a property as boolean.
|
||||
*/
|
||||
def propertyBool(name: String): UIO[Option[Boolean]] =
|
||||
property(name).map(_.flatMap(_.asBoolean))
|
||||
|
||||
/** Environment available to components */
|
||||
type ComponentEnv = PageContext
|
||||
```
|
||||
|
||||
**ComponentFactory.scala:**
|
||||
```scala
|
||||
package component
|
||||
|
||||
import zio.*
|
||||
import java.nio.file.Path
|
||||
|
||||
/** Factory for creating component instances */
|
||||
trait ComponentFactory:
|
||||
/** Component class name (fully qualified) */
|
||||
def className: String
|
||||
|
||||
/** Schema parsed from component.yaml */
|
||||
def schema: ComponentSchema
|
||||
|
||||
/** Path to component's directory (contains templates) */
|
||||
def directory: Path
|
||||
|
||||
/** Create a component instance with given alias, properties, and context */
|
||||
def create(
|
||||
alias: String,
|
||||
initialProperties: Map[String, PropertyValue],
|
||||
ctx: PageContext
|
||||
): UIO[SummerComponent]
|
||||
|
||||
object ComponentFactory:
|
||||
/**
|
||||
* Create a factory from a component class and schema.
|
||||
* The factory will create instances with the given properties.
|
||||
*/
|
||||
def apply[C <: SummerComponent](
|
||||
cls: Class[C],
|
||||
schema: ComponentSchema,
|
||||
directory: Path,
|
||||
constructor: (String, Map[String, PropertyDef], Ref[Map[String, PropertyValue]], PageContext, Path) => C
|
||||
): ComponentFactory = new ComponentFactory:
|
||||
val className = cls.getName
|
||||
val schema_ = schema
|
||||
def schema = schema_
|
||||
val directory_ = directory
|
||||
def directory = directory_
|
||||
|
||||
def create(
|
||||
alias: String,
|
||||
initialProperties: Map[String, PropertyValue],
|
||||
ctx: PageContext
|
||||
): UIO[SummerComponent] =
|
||||
Ref.make(initialProperties).map { propsRef =>
|
||||
constructor(alias, schema.properties, propsRef, ctx, directory)
|
||||
}
|
||||
```
|
||||
|
||||
**ComponentManager.scala:**
|
||||
```scala
|
||||
package component
|
||||
|
||||
import zio.*
|
||||
import plugin.{PluginId, PluginManager}
|
||||
|
||||
/** Registration info for a component */
|
||||
case class ComponentRegistration(
|
||||
pluginId: PluginId,
|
||||
alias: String,
|
||||
factory: ComponentFactory
|
||||
)
|
||||
|
||||
/** Service for managing all registered components */
|
||||
trait ComponentManager:
|
||||
/** List all registered components */
|
||||
def listComponents: UIO[List[ComponentRegistration]]
|
||||
|
||||
/** Find a component by alias */
|
||||
def resolve(alias: String): UIO[Option[ComponentRegistration]]
|
||||
|
||||
/** Find a component by alias or class name */
|
||||
def resolveByAliasOrClass(aliasOrClass: String): UIO[Option[ComponentRegistration]]
|
||||
|
||||
/** Create a component instance */
|
||||
def create(
|
||||
alias: String,
|
||||
properties: Map[String, PropertyValue],
|
||||
ctx: PageContext
|
||||
): IO[ComponentError, SummerComponent]
|
||||
|
||||
/** Register a component from a plugin */
|
||||
def register(pluginId: PluginId, alias: String, factory: ComponentFactory): UIO[Unit]
|
||||
|
||||
object ComponentManager:
|
||||
/** List all components */
|
||||
def listComponents: ZIO[ComponentManager, Nothing, List[ComponentRegistration]] =
|
||||
ZIO.serviceWithZIO[ComponentManager](_.listComponents)
|
||||
|
||||
/** Resolve component by alias */
|
||||
def resolve(alias: String): ZIO[ComponentManager, Nothing, Option[ComponentRegistration]] =
|
||||
ZIO.serviceWithZIO[ComponentManager](_.resolve(alias))
|
||||
|
||||
/** Create component instance */
|
||||
def create(
|
||||
alias: String,
|
||||
properties: Map[String, PropertyValue],
|
||||
ctx: PageContext
|
||||
): ZIO[ComponentManager, ComponentError, SummerComponent] =
|
||||
ZIO.serviceWithZIO[ComponentManager](_.create(alias, properties, ctx))
|
||||
|
||||
/** Live implementation */
|
||||
val live: ULayer[ComponentManager] = ZLayer.fromZIO {
|
||||
Ref.make(Map.empty[String, ComponentRegistration]).map { registry =>
|
||||
new ComponentManager:
|
||||
def listComponents: UIO[List[ComponentRegistration]] =
|
||||
registry.get.map(_.values.toList)
|
||||
|
||||
def resolve(alias: String): UIO[Option[ComponentRegistration]] =
|
||||
registry.get.map(_.get(alias))
|
||||
|
||||
def resolveByAliasOrClass(aliasOrClass: String): UIO[Option[ComponentRegistration]] =
|
||||
registry.get.map { regs =>
|
||||
regs.get(aliasOrClass).orElse(
|
||||
regs.values.find(_.factory.className == aliasOrClass)
|
||||
)
|
||||
}
|
||||
|
||||
def create(
|
||||
alias: String,
|
||||
properties: Map[String, PropertyValue],
|
||||
ctx: PageContext
|
||||
): IO[ComponentError, SummerComponent] =
|
||||
resolve(alias).flatMap {
|
||||
case Some(reg) => reg.factory.create(alias, properties, ctx)
|
||||
case None => ZIO.fail(ComponentError.ComponentNotFound(alias))
|
||||
}
|
||||
|
||||
def register(pluginId: PluginId, alias: String, factory: ComponentFactory): UIO[Unit] =
|
||||
registry.update(_ + (alias -> ComponentRegistration(pluginId, alias, factory)))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Update PluginRegistration.scala** - read existing file and replace ComponentDef with proper import:
|
||||
|
||||
Read the existing file from Phase 2 and update the ComponentDef to use the component package:
|
||||
|
||||
```scala
|
||||
package plugin
|
||||
|
||||
import component.ComponentFactory
|
||||
|
||||
/** Data returned from plugin's register() method - declarative, no effects */
|
||||
case class PluginRegistration(
|
||||
// Components provided by this plugin (alias -> factory)
|
||||
components: Map[String, ComponentFactory] = Map.empty,
|
||||
// Permissions defined by this plugin (Phase 6)
|
||||
permissions: List[PermissionDef] = List.empty,
|
||||
// Navigation items for admin backend (Phase 8)
|
||||
navigation: List[NavigationDef] = List.empty,
|
||||
// Settings pages (Phase 8)
|
||||
settings: List[SettingDef] = List.empty,
|
||||
// Event subscriptions (Phase 2 extension API)
|
||||
events: List[EventSubscription] = List.empty,
|
||||
// Extensions to other plugins (Phase 2 extension API)
|
||||
extensions: List[ExtensionDef] = List.empty,
|
||||
// Form field definitions (for YAML-driven forms)
|
||||
fields: List[FieldDef] = List.empty
|
||||
)
|
||||
|
||||
object PluginRegistration:
|
||||
val empty: PluginRegistration = PluginRegistration()
|
||||
|
||||
// Placeholder types - will be fleshed out in later phases
|
||||
case class PermissionDef(code: String, label: String)
|
||||
case class NavigationDef(id: String, label: String, url: String, icon: String = "", order: Int = 0)
|
||||
case class SettingDef(key: String, label: String, icon: String = "")
|
||||
case class EventSubscription(eventType: String, handler: String)
|
||||
case class ExtensionDef(target: String, extensionClass: String)
|
||||
case class FieldDef(name: String, fieldType: String)
|
||||
```
|
||||
|
||||
Note: Changed `components: List[ComponentDef]` to `components: Map[String, ComponentFactory]` for proper registration pattern.
|
||||
</action>
|
||||
<verify>
|
||||
Run `./mill summercms.compile` - all component and plugin files compile.
|
||||
|
||||
Verify the types work together by checking in Mill REPL:
|
||||
```bash
|
||||
./mill -i summercms.console
|
||||
```
|
||||
|
||||
Then:
|
||||
```scala
|
||||
import component._
|
||||
import zio._
|
||||
|
||||
// Test schema parsing
|
||||
val yaml = """
|
||||
name: Test Component
|
||||
description: A test component
|
||||
properties:
|
||||
title:
|
||||
type: String
|
||||
title: Title
|
||||
default: "Hello"
|
||||
"""
|
||||
println(ComponentSchema.parse(yaml))
|
||||
// Should print Right(ComponentSchema(Test Component,A test component,None,Map(title -> PropertyDef(...))))
|
||||
|
||||
:quit
|
||||
```
|
||||
</verify>
|
||||
<done>SummerComponent trait with lifecycle hooks, ComponentFactory, ComponentManager service, PluginRegistration updated to use Map[String, ComponentFactory]</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `./mill summercms.compile` succeeds with all new dependencies and types
|
||||
2. ComponentSchema.parse(validYaml) returns Right(schema)
|
||||
3. PropertyDef parses type, title, default, validation from YAML
|
||||
4. SummerComponent trait has init and onRun lifecycle hooks
|
||||
5. ComponentManager.live provides working service
|
||||
6. PluginRegistration.components is Map[String, ComponentFactory]
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- build.mill has Pebble, zio-http-htmx, ScalaTags dependencies
|
||||
- YAML property schemas parse with all WinterCMS-compatible types
|
||||
- SummerComponent trait defines alias, details, propertySchema, properties, init, onRun
|
||||
- ComponentManager can register and resolve components by alias
|
||||
- PluginRegistration supports component registration via Map
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-component-system/03-01-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user