Changeset 0.35.0 (#209)

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
2026-03-20 09:59:53 +00:00
committed by xcaliber
parent d16bb93177
commit bf8082e69f
37 changed files with 2324 additions and 129 deletions

View File

@@ -62,6 +62,7 @@ type WorkflowStage struct {
AutoTransition bool `json:"auto_transition"`
TransitionRules json.RawMessage `json:"transition_rules"`
SurfacePkgID *string `json:"surface_pkg_id,omitempty"`
SLASeconds *int `json:"sla_seconds,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
@@ -85,9 +86,26 @@ var ValidStageModes = map[string]bool{
// ── Typed Form Template ─────────────────────
// TypedFormTemplate is the structured form_template schema (v0.29.3).
// v0.35.0: added Fieldsets for progressive multi-step forms.
type TypedFormTemplate struct {
Fields []FormField `json:"fields"`
Fieldsets []FormFieldset `json:"fieldsets,omitempty"` // v0.35.0: multi-step form groups
Hooks *FormHooks `json:"hooks,omitempty"`
}
// FormFieldset groups fields into a labelled step for progressive forms (v0.35.0).
// When fieldsets is present, top-level fields is ignored.
type FormFieldset struct {
Label string `json:"label"`
Fields []FormField `json:"fields"`
Hooks *FormHooks `json:"hooks,omitempty"`
}
// FieldCondition controls conditional visibility of a form field (v0.35.0).
// When set, the field is shown/validated only if the condition is met.
type FieldCondition struct {
When string `json:"when"` // key of the field to check
Op string `json:"op,omitempty"` // eq (default) | neq | in | exists
Value any `json:"value,omitempty"` // value to compare against
}
// FormField is a single field in a typed form template.
@@ -100,6 +118,7 @@ type FormField struct {
Default interface{} `json:"default,omitempty"`
Options []FormOption `json:"options,omitempty"`
Validation *FormValidation `json:"validation,omitempty"`
Condition *FieldCondition `json:"condition,omitempty"` // v0.35.0: conditional visibility
}
// FormOption is a choice for select fields.
@@ -145,6 +164,19 @@ func ParseTypedFormTemplate(raw json.RawMessage) *TypedFormTemplate {
if err := json.Unmarshal(raw, &tpl); err != nil {
return nil
}
// v0.35.0: If fieldsets present, flatten into fields for backward compat
if len(tpl.Fieldsets) > 0 {
var allFields []FormField
for _, fs := range tpl.Fieldsets {
allFields = append(allFields, fs.Fields...)
}
if len(allFields) > 0 {
tpl.Fields = allFields
return &tpl
}
}
if len(tpl.Fields) == 0 {
return nil
}
@@ -155,6 +187,11 @@ func ParseTypedFormTemplate(raw json.RawMessage) *TypedFormTemplate {
return &tpl
}
// AllFields returns all fields from the template, whether from top-level fields or fieldsets.
func (t *TypedFormTemplate) AllFields() []FormField {
return t.Fields // ParseTypedFormTemplate already flattens fieldsets into Fields
}
// FieldError is a validation error for a specific form field.
type FieldError struct {
Key string `json:"key"`
@@ -165,6 +202,11 @@ type FieldError struct {
func ValidateFormData(tpl *TypedFormTemplate, data map[string]interface{}) []FieldError {
var errs []FieldError
for _, f := range tpl.Fields {
// v0.35.0: Skip fields whose condition is not met
if f.Condition != nil && !evaluateFieldCondition(f.Condition, data) {
continue
}
val, present := data[f.Key]
// Required check
@@ -315,6 +357,49 @@ func validateSelect(f FormField, val interface{}) []FieldError {
return []FieldError{{Key: f.Key, Message: f.Label + " is not a valid option"}}
}
// ── Field Condition Evaluator (v0.35.0) ─────
// evaluateFieldCondition checks if a conditional field should be visible/validated.
func evaluateFieldCondition(cond *FieldCondition, data map[string]interface{}) bool {
val, exists := data[cond.When]
op := cond.Op
if op == "" {
op = "eq"
}
switch op {
case "exists":
return exists
case "eq":
if !exists {
return false
}
return fmt.Sprintf("%v", val) == fmt.Sprintf("%v", cond.Value)
case "neq":
if !exists {
return true
}
return fmt.Sprintf("%v", val) != fmt.Sprintf("%v", cond.Value)
case "in":
if !exists {
return false
}
arr, ok := cond.Value.([]interface{})
if !ok {
return false
}
vs := fmt.Sprintf("%v", val)
for _, item := range arr {
if fmt.Sprintf("%v", item) == vs {
return true
}
}
return false
default:
return true
}
}
// ── Workflow Version (immutable snapshot) ───
// WorkflowVersion is an immutable snapshot of a workflow definition