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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user