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
This commit is contained in:
526
.planning/phases/07-admin-forms-lists/07-02-PLAN.md
Normal file
526
.planning/phases/07-admin-forms-lists/07-02-PLAN.md
Normal file
@@ -0,0 +1,526 @@
|
||||
---
|
||||
phase: 07-admin-forms-lists
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["07-01"]
|
||||
files_modified:
|
||||
- summercms/src/admin/forms/widgets/TextWidget.scala
|
||||
- summercms/src/admin/forms/widgets/TextareaWidget.scala
|
||||
- summercms/src/admin/forms/widgets/DropdownWidget.scala
|
||||
- summercms/src/admin/forms/widgets/CheckboxWidget.scala
|
||||
- summercms/src/admin/forms/widgets/DatePickerWidget.scala
|
||||
- summercms/src/admin/forms/widgets/RepeaterWidget.scala
|
||||
- summercms/src/admin/forms/widgets/RelationWidget.scala
|
||||
- summercms/src/admin/forms/WidgetBootstrap.scala
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Text input renders with proper attributes (placeholder, required, disabled)"
|
||||
- "Textarea renders with rows/cols and comment help text"
|
||||
- "Dropdown renders select element with options from config or method"
|
||||
- "Checkbox renders with label alignment and checked state"
|
||||
- "DatePicker includes Flatpickr initialization attributes"
|
||||
- "Repeater renders item list with add/remove/reorder controls"
|
||||
- "Relation widget renders modal trigger with list display"
|
||||
artifacts:
|
||||
- path: "summercms/src/admin/forms/widgets/TextWidget.scala"
|
||||
provides: "Text input widget (text, email, password, number, url types)"
|
||||
exports: ["TextWidget"]
|
||||
- path: "summercms/src/admin/forms/widgets/TextareaWidget.scala"
|
||||
provides: "Textarea widget with size variants"
|
||||
exports: ["TextareaWidget"]
|
||||
- path: "summercms/src/admin/forms/widgets/DropdownWidget.scala"
|
||||
provides: "Select dropdown widget with options"
|
||||
exports: ["DropdownWidget"]
|
||||
- path: "summercms/src/admin/forms/widgets/CheckboxWidget.scala"
|
||||
provides: "Checkbox and switch widgets"
|
||||
exports: ["CheckboxWidget", "SwitchWidget"]
|
||||
- path: "summercms/src/admin/forms/widgets/DatePickerWidget.scala"
|
||||
provides: "Date/time picker with Flatpickr integration"
|
||||
exports: ["DatePickerWidget"]
|
||||
- path: "summercms/src/admin/forms/widgets/RepeaterWidget.scala"
|
||||
provides: "Repeater widget with nested form fields"
|
||||
exports: ["RepeaterWidget"]
|
||||
- path: "summercms/src/admin/forms/widgets/RelationWidget.scala"
|
||||
provides: "Relation widget with modal management"
|
||||
exports: ["RelationWidget"]
|
||||
- path: "summercms/src/admin/forms/WidgetBootstrap.scala"
|
||||
provides: "Registers all standard widgets at startup"
|
||||
exports: ["WidgetBootstrap"]
|
||||
key_links:
|
||||
- from: "summercms/src/admin/forms/widgets/*.scala"
|
||||
to: "summercms/src/admin/forms/WidgetRegistry.scala"
|
||||
via: "implements FormWidget trait"
|
||||
pattern: "extends FormWidget"
|
||||
- from: "summercms/src/admin/forms/WidgetBootstrap.scala"
|
||||
to: "summercms/src/admin/forms/WidgetRegistry.scala"
|
||||
via: "registers widgets on boot"
|
||||
pattern: "registry\\.register"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement all standard form widgets for the admin backend.
|
||||
|
||||
Purpose: This plan creates the widget implementations that render actual form inputs. Each widget type handles its specific UI requirements, from simple text inputs to complex repeaters and relation modals.
|
||||
|
||||
Output: Complete set of form widgets (text, textarea, dropdown, checkbox, datepicker, repeater, relation) that integrate with the WidgetRegistry from Plan 07-01.
|
||||
</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
|
||||
@.planning/phases/07-admin-forms-lists/07-01-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Implement basic input widgets (Text, Textarea, Dropdown, Checkbox)</name>
|
||||
<files>
|
||||
summercms/src/admin/forms/widgets/TextWidget.scala
|
||||
summercms/src/admin/forms/widgets/TextareaWidget.scala
|
||||
summercms/src/admin/forms/widgets/DropdownWidget.scala
|
||||
summercms/src/admin/forms/widgets/CheckboxWidget.scala
|
||||
</files>
|
||||
<action>
|
||||
Create TextWidget.scala:
|
||||
- Implements FormWidget trait
|
||||
- Handles multiple input types via field.type: text, email, password, number, url
|
||||
- Renders input element with:
|
||||
- type attribute based on field type
|
||||
- name attribute for form submission
|
||||
- value from context data
|
||||
- placeholder from field.placeholder
|
||||
- required attribute if field.required
|
||||
- disabled attribute if field.disabled
|
||||
- readonly attribute if field.readOnly
|
||||
- Custom attributes from field.attributes map
|
||||
- CSS classes: "form-control" base + field.cssClass
|
||||
|
||||
```scala
|
||||
object TextWidget extends FormWidget:
|
||||
def render(field: FormField, name: String, value: Any, ctx: FormContext): Frag =
|
||||
val inputType = field.fieldType match
|
||||
case "password" => "password"
|
||||
case "email" => "email"
|
||||
case "url" => "url"
|
||||
case "number" => "number"
|
||||
case _ => "text"
|
||||
|
||||
input(
|
||||
cls := s"form-control ${field.cssClass.getOrElse("")}",
|
||||
tpe := inputType,
|
||||
attr("name") := name,
|
||||
attr("value") := Option(value).map(_.toString).getOrElse(""),
|
||||
// ... additional attributes
|
||||
)
|
||||
```
|
||||
|
||||
Create TextareaWidget.scala:
|
||||
- Renders textarea element
|
||||
- Supports size variant: tiny (2 rows), small (4), default (6), large (10), huge (15), giant (20)
|
||||
- Size determined by field.size property
|
||||
- Content goes inside textarea tags, not value attribute
|
||||
|
||||
Create DropdownWidget.scala:
|
||||
- Renders select element with option children
|
||||
- Options from field.options (Map[String, String] from YAML)
|
||||
- Support emptyOption for "-- Select --" placeholder
|
||||
- Mark selected option based on current value
|
||||
- Handle multiple selection if field indicates (future enhancement)
|
||||
|
||||
```scala
|
||||
object DropdownWidget extends FormWidget:
|
||||
def render(field: FormField, name: String, value: Any, ctx: FormContext): Frag =
|
||||
val currentValue = Option(value).map(_.toString).getOrElse("")
|
||||
|
||||
select(
|
||||
cls := s"form-control ${field.cssClass.getOrElse("")}",
|
||||
attr("name") := name,
|
||||
field.required.filter(identity).map(_ => attr("required") := "required"),
|
||||
|
||||
field.emptyOption.map(empty => option(attr("value") := "", empty)),
|
||||
|
||||
field.options.map(_.toSeq.map { case (k, v) =>
|
||||
option(
|
||||
attr("value") := k,
|
||||
if k == currentValue then Some(attr("selected") := "selected") else None,
|
||||
v
|
||||
)
|
||||
}).getOrElse(Nil)
|
||||
)
|
||||
```
|
||||
|
||||
Create CheckboxWidget.scala:
|
||||
- Renders checkbox input with label wrapper
|
||||
- Also supports "switch" type for toggle-style checkbox
|
||||
- Checkbox value handling: hidden input with "0" + checkbox with "1" (standard pattern)
|
||||
- Checked state based on truthy value (true, 1, "1", "yes", "on")
|
||||
|
||||
```scala
|
||||
object CheckboxWidget extends FormWidget:
|
||||
def render(field: FormField, name: String, value: Any, ctx: FormContext): Frag =
|
||||
val isChecked = value match
|
||||
case b: Boolean => b
|
||||
case n: Number => n.intValue() != 0
|
||||
case s: String => Set("true", "1", "yes", "on").contains(s.toLowerCase)
|
||||
case _ => false
|
||||
|
||||
div(cls := "checkbox-wrapper",
|
||||
input(tpe := "hidden", attr("name") := name, attr("value") := "0"),
|
||||
label(cls := "checkbox-label",
|
||||
input(
|
||||
tpe := "checkbox",
|
||||
attr("name") := name,
|
||||
attr("value") := "1",
|
||||
if isChecked then Some(attr("checked") := "checked") else None
|
||||
),
|
||||
span(field.label.getOrElse(name))
|
||||
)
|
||||
)
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
Run `mill summercms.compile` - should compile without errors.
|
||||
Verify each widget produces expected HTML structure.
|
||||
</verify>
|
||||
<done>
|
||||
Basic input widgets (Text, Textarea, Dropdown, Checkbox) implemented with proper attribute handling, value binding, and CSS classes.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Implement DatePicker and Repeater widgets</name>
|
||||
<files>
|
||||
summercms/src/admin/forms/widgets/DatePickerWidget.scala
|
||||
summercms/src/admin/forms/widgets/RepeaterWidget.scala
|
||||
</files>
|
||||
<action>
|
||||
Create DatePickerWidget.scala:
|
||||
- Renders text input with Flatpickr data attributes for JS initialization
|
||||
- Support modes: date, datetime, time (via field.mode property)
|
||||
- Data attributes for Flatpickr configuration:
|
||||
- data-flatpickr: marker for JS initialization
|
||||
- data-enable-time: for datetime/time modes
|
||||
- data-no-calendar: for time-only mode
|
||||
- data-date-format: based on mode (Y-m-d, Y-m-d H:i, H:i)
|
||||
- data-min-date, data-max-date: if specified in field config
|
||||
|
||||
```scala
|
||||
object DatePickerWidget extends FormWidget:
|
||||
def render(field: FormField, name: String, value: Any, ctx: FormContext): Frag =
|
||||
val mode = field.mode.getOrElse("date")
|
||||
val (enableTime, noCalendar, format) = mode match
|
||||
case "datetime" => (true, false, "Y-m-d H:i")
|
||||
case "time" => (true, true, "H:i")
|
||||
case _ => (false, false, "Y-m-d")
|
||||
|
||||
input(
|
||||
cls := s"form-control flatpickr-input ${field.cssClass.getOrElse("")}",
|
||||
tpe := "text",
|
||||
attr("name") := name,
|
||||
attr("value") := Option(value).map(_.toString).getOrElse(""),
|
||||
data("flatpickr") := "true",
|
||||
if enableTime then Some(data("enable-time") := "true") else None,
|
||||
if noCalendar then Some(data("no-calendar") := "true") else None,
|
||||
data("date-format") := format
|
||||
)
|
||||
```
|
||||
|
||||
Create RepeaterWidget.scala (complex widget per CONTEXT.md):
|
||||
- Renders list of repeater items with nested form fields
|
||||
- Each item has:
|
||||
- Collapse/expand toggle (items collapsible per CONTEXT.md)
|
||||
- Title from field.titleFrom (shows value of specified field as item title)
|
||||
- Drag handle for reordering (Sortable.js data attributes)
|
||||
- Remove button
|
||||
- Nested form fields from field.form.fields
|
||||
- Below items:
|
||||
- Add button with prompt text from field.prompt
|
||||
- Item wrapper with unique UUID-based keys (not sequential indexes per RESEARCH.md)
|
||||
- Respects minItems/maxItems constraints (disable add/remove accordingly)
|
||||
- Data attributes for Sortable.js: data-sortable, data-handle
|
||||
|
||||
```scala
|
||||
object RepeaterWidget extends FormWidget:
|
||||
def render(field: FormField, name: String, value: Any, ctx: FormContext): Frag =
|
||||
val items = value match
|
||||
case list: List[_] => list.asInstanceOf[List[Map[String, Any]]]
|
||||
case _ => List.empty[Map[String, Any]]
|
||||
|
||||
val nestedFields = field.form.map(_.fields).getOrElse(Map.empty)
|
||||
val minItems = field.minItems.getOrElse(0)
|
||||
val maxItems = field.maxItems.getOrElse(Int.MaxValue)
|
||||
|
||||
div(cls := "repeater-widget",
|
||||
data("repeater") := "true",
|
||||
data("min-items") := minItems.toString,
|
||||
data("max-items") := maxItems.toString,
|
||||
|
||||
div(cls := "repeater-items", data("sortable") := "true",
|
||||
items.zipWithIndex.map { case (itemData, idx) =>
|
||||
val itemKey = itemData.getOrElse("_key", java.util.UUID.randomUUID().toString)
|
||||
renderRepeaterItem(field, name, itemKey.toString, itemData, nestedFields, idx, ctx)
|
||||
}
|
||||
),
|
||||
|
||||
div(cls := "repeater-add",
|
||||
button(
|
||||
tpe := "button",
|
||||
cls := "btn btn-secondary btn-add-item",
|
||||
attr("hx-get") := s"/admin/repeater/add-item?field=$name",
|
||||
attr("hx-target") := ".repeater-items",
|
||||
attr("hx-swap") := "beforeend",
|
||||
if items.size >= maxItems then Some(attr("disabled") := "disabled") else None,
|
||||
field.prompt.getOrElse("Add Item")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
private def renderRepeaterItem(
|
||||
field: FormField,
|
||||
baseName: String,
|
||||
itemKey: String,
|
||||
itemData: Map[String, Any],
|
||||
nestedFields: Map[String, FormFieldConfig],
|
||||
index: Int,
|
||||
ctx: FormContext
|
||||
): Frag =
|
||||
div(cls := "repeater-item", data("item-key") := itemKey,
|
||||
div(cls := "repeater-item-header",
|
||||
span(cls := "drag-handle", data("handle") := "true", "::"),
|
||||
span(cls := "item-title",
|
||||
field.titleFrom.flatMap(tf => itemData.get(tf).map(_.toString)).getOrElse(s"Item ${index + 1}")
|
||||
),
|
||||
button(tpe := "button", cls := "btn-collapse", data("toggle") := "collapse", "-"),
|
||||
button(tpe := "button", cls := "btn-remove", data("action") := "remove", "x")
|
||||
),
|
||||
div(cls := "repeater-item-body",
|
||||
nestedFields.map { case (fieldName, fieldConfig) =>
|
||||
val nestedName = s"${baseName}[$itemKey][$fieldName]"
|
||||
val nestedValue = itemData.getOrElse(fieldName, null)
|
||||
// Render nested field (simplified - would use FormRenderer in real impl)
|
||||
renderNestedField(fieldConfig, nestedName, nestedValue, ctx)
|
||||
}
|
||||
),
|
||||
input(tpe := "hidden", attr("name") := s"${baseName}[$itemKey][_key]", attr("value") := itemKey)
|
||||
)
|
||||
```
|
||||
|
||||
Note: Repeater is complex - the implementation should include:
|
||||
- HTMX endpoint for adding new items dynamically
|
||||
- JavaScript for Sortable.js initialization
|
||||
- Proper name indexing for form submission
|
||||
</action>
|
||||
<verify>
|
||||
Run `mill summercms.compile` - should compile without errors.
|
||||
Verify DatePicker produces input with Flatpickr data attributes.
|
||||
Verify Repeater produces item list structure with controls.
|
||||
</verify>
|
||||
<done>
|
||||
DatePicker widget renders with Flatpickr integration attributes. Repeater widget renders item list with collapse, drag-reorder, add/remove controls and nested field support.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Implement Relation widget and Widget bootstrap</name>
|
||||
<files>
|
||||
summercms/src/admin/forms/widgets/RelationWidget.scala
|
||||
summercms/src/admin/forms/WidgetBootstrap.scala
|
||||
</files>
|
||||
<action>
|
||||
Create RelationWidget.scala (RelationController-style per CONTEXT.md):
|
||||
- Modal-based relation management (not simple dropdown)
|
||||
- Renders:
|
||||
- Display area showing currently selected related records
|
||||
- Buttons: "Link", "Create" (if allowed), "Remove" for selected
|
||||
- Hidden inputs with selected relation IDs
|
||||
- Modal loaded via HTMX on button click
|
||||
- Supports relation types: belongsTo, hasOne, hasMany, belongsToMany
|
||||
|
||||
```scala
|
||||
object RelationWidget extends FormWidget:
|
||||
def render(field: FormField, name: String, value: Any, ctx: FormContext): Frag =
|
||||
val selectedIds = value match
|
||||
case list: List[_] => list.map(_.toString)
|
||||
case single if single != null => List(single.toString)
|
||||
case _ => List.empty
|
||||
|
||||
val relationName = field.nameFrom.getOrElse("name")
|
||||
val sessionKey = ctx.sessionKey
|
||||
|
||||
div(cls := "relation-widget",
|
||||
data("relation") := "true",
|
||||
data("session-key") := sessionKey,
|
||||
|
||||
// Display selected relations
|
||||
div(cls := "relation-items",
|
||||
selectedIds.map(id => renderRelationItem(name, id, relationName))
|
||||
),
|
||||
|
||||
// Hidden inputs for form submission
|
||||
selectedIds.map(id =>
|
||||
input(tpe := "hidden", attr("name") := s"${name}[]", attr("value") := id)
|
||||
),
|
||||
|
||||
// Action buttons
|
||||
div(cls := "relation-actions",
|
||||
button(
|
||||
tpe := "button",
|
||||
cls := "btn btn-secondary btn-link-relation",
|
||||
attr("hx-get") := s"/admin/relation/modal?field=$name&session=$sessionKey",
|
||||
attr("hx-target") := "#relation-modal .modal-content",
|
||||
attr("hx-trigger") := "click",
|
||||
"Link Existing"
|
||||
),
|
||||
button(
|
||||
tpe := "button",
|
||||
cls := "btn btn-secondary btn-create-relation",
|
||||
attr("hx-get") := s"/admin/relation/create-modal?field=$name&session=$sessionKey",
|
||||
attr("hx-target") := "#relation-modal .modal-content",
|
||||
attr("hx-trigger") := "click",
|
||||
"Create New"
|
||||
)
|
||||
),
|
||||
|
||||
// Modal placeholder
|
||||
div(id := "relation-modal", cls := "modal",
|
||||
div(cls := "modal-content")
|
||||
)
|
||||
)
|
||||
|
||||
private def renderRelationItem(name: String, id: String, displayAttr: String): Frag =
|
||||
div(cls := "relation-item", data("id") := id,
|
||||
span(cls := "item-display", s"Loading..."), // Would be populated via HTMX or initial load
|
||||
button(
|
||||
tpe := "button",
|
||||
cls := "btn-remove-relation",
|
||||
data("action") := "remove",
|
||||
data("id") := id,
|
||||
"x"
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
Create WidgetBootstrap.scala:
|
||||
- Registers all standard widgets with the WidgetRegistry on application boot
|
||||
- Can be called from app initialization or plugin boot lifecycle
|
||||
|
||||
```scala
|
||||
object WidgetBootstrap:
|
||||
def register: ZIO[WidgetRegistry, Nothing, Unit] =
|
||||
for
|
||||
registry <- ZIO.service[WidgetRegistry]
|
||||
_ <- registry.register("text", TextWidget)
|
||||
_ <- registry.register("password", TextWidget) // Same widget, different type attr
|
||||
_ <- registry.register("email", TextWidget)
|
||||
_ <- registry.register("url", TextWidget)
|
||||
_ <- registry.register("number", TextWidget)
|
||||
_ <- registry.register("textarea", TextareaWidget)
|
||||
_ <- registry.register("dropdown", DropdownWidget)
|
||||
_ <- registry.register("select", DropdownWidget) // Alias
|
||||
_ <- registry.register("checkbox", CheckboxWidget)
|
||||
_ <- registry.register("switch", SwitchWidget)
|
||||
_ <- registry.register("datepicker", DatePickerWidget)
|
||||
_ <- registry.register("date", DatePickerWidget) // Alias
|
||||
_ <- registry.register("datetime", DatePickerWidget)
|
||||
_ <- registry.register("time", DatePickerWidget)
|
||||
_ <- registry.register("repeater", RepeaterWidget)
|
||||
_ <- registry.register("relation", RelationWidget)
|
||||
yield ()
|
||||
|
||||
val layer: ZLayer[WidgetRegistry, Nothing, Unit] =
|
||||
ZLayer.fromZIO(register)
|
||||
```
|
||||
|
||||
Also create SwitchWidget in CheckboxWidget.scala file (variant of checkbox with different styling):
|
||||
```scala
|
||||
object SwitchWidget extends FormWidget:
|
||||
// Similar to CheckboxWidget but with switch CSS classes
|
||||
def render(field: FormField, name: String, value: Any, ctx: FormContext): Frag =
|
||||
val isChecked = CheckboxWidget.isValueChecked(value)
|
||||
div(cls := "switch-wrapper",
|
||||
label(cls := "switch",
|
||||
input(tpe := "hidden", attr("name") := name, attr("value") := "0"),
|
||||
input(
|
||||
tpe := "checkbox",
|
||||
attr("name") := name,
|
||||
attr("value") := "1",
|
||||
if isChecked then Some(attr("checked") := "checked") else None
|
||||
),
|
||||
span(cls := "slider")
|
||||
)
|
||||
)
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
Run `mill summercms.compile` - should compile without errors.
|
||||
Verify RelationWidget produces modal trigger structure.
|
||||
Verify WidgetBootstrap registers all widgets when called.
|
||||
</verify>
|
||||
<done>
|
||||
RelationWidget implements modal-based relation management with link/create actions. WidgetBootstrap registers all standard widgets (text, textarea, dropdown, checkbox, switch, datepicker, repeater, relation) with the registry.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
After completing all tasks:
|
||||
|
||||
1. **Compile check:**
|
||||
```bash
|
||||
mill summercms.compile
|
||||
```
|
||||
Must succeed with no errors.
|
||||
|
||||
2. **Widget output verification:**
|
||||
For each widget, verify rendered HTML contains:
|
||||
- TextWidget: input[type="text"] with form-control class
|
||||
- TextareaWidget: textarea element with rows attribute
|
||||
- DropdownWidget: select element with option children
|
||||
- CheckboxWidget: hidden input + checkbox input with label
|
||||
- DatePickerWidget: input with data-flatpickr attribute
|
||||
- RepeaterWidget: .repeater-items container with add button
|
||||
- RelationWidget: .relation-items with modal trigger buttons
|
||||
|
||||
3. **Widget registration verification:**
|
||||
After WidgetBootstrap.register runs, verify:
|
||||
- registry.resolve("text") returns Some(TextWidget)
|
||||
- registry.resolve("textarea") returns Some(TextareaWidget)
|
||||
- registry.resolve("dropdown") returns Some(DropdownWidget)
|
||||
- All registered types are resolvable
|
||||
|
||||
4. **Integration verification:**
|
||||
Use FormRenderer from 07-01 with sample fields.yaml containing various field types.
|
||||
Verify form renders with appropriate widgets for each field type.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] TextWidget handles text, email, password, number, url input types
|
||||
- [ ] TextareaWidget supports size variants (tiny, small, default, large, huge, giant)
|
||||
- [ ] DropdownWidget renders select with options from config, handles emptyOption
|
||||
- [ ] CheckboxWidget renders checkbox with hidden field pattern for proper form submission
|
||||
- [ ] SwitchWidget provides toggle-style checkbox variant
|
||||
- [ ] DatePickerWidget includes Flatpickr data attributes for JS initialization
|
||||
- [ ] RepeaterWidget renders item list with collapse, drag handle, add/remove
|
||||
- [ ] Repeater uses UUID keys (not sequential indexes) for items
|
||||
- [ ] RelationWidget renders modal triggers for link/create actions
|
||||
- [ ] WidgetBootstrap registers all widgets with proper type mappings
|
||||
- [ ] All widgets implement FormWidget trait consistently
|
||||
- [ ] Project compiles successfully with `mill summercms.compile`
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/07-admin-forms-lists/07-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user