allow for sections and components in any order on pages
This commit is contained in:
@ -4,13 +4,121 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseAdvancedUIFeatures(t *testing.T) {
|
||||
func TestParseAdvancedUIStructures(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want AST
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "complex form with validation and conditional fields",
|
||||
input: `page Test at "/test" layout main {
|
||||
component form for User {
|
||||
field name type text required validate min_length "3"
|
||||
field email type email required validate email
|
||||
field account_type type select options ["personal", "business"] default "personal"
|
||||
when account_type equals "business" {
|
||||
field company_name type text required
|
||||
field tax_id type text validate pattern "[0-9]{9}"
|
||||
}
|
||||
button submit label "Create Account" style "primary"
|
||||
button cancel label "Cancel" style "secondary"
|
||||
}
|
||||
}`,
|
||||
want: AST{
|
||||
Definitions: []Definition{
|
||||
{
|
||||
Page: &Page{
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
{Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "email",
|
||||
Type: "email",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
{Validation: &ComponentValidation{Type: "email"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "account_type",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Options: []string{"personal", "business"}},
|
||||
{Default: stringPtr("personal")},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "account_type",
|
||||
Operator: "equals",
|
||||
Value: "business",
|
||||
Fields: []ComponentField{
|
||||
{
|
||||
Name: "company_name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tax_id",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Validation: &ComponentValidation{Type: "pattern", Value: stringPtr("[0-9]{9}")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "submit",
|
||||
Label: "Create Account",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "primary"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "cancel",
|
||||
Label: "Cancel",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "secondary"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complex conditional rendering with multiple operators",
|
||||
input: `page Test at "/test" layout main {
|
||||
@ -42,86 +150,88 @@ func TestParseAdvancedUIFeatures(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Components: []Component{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "status",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Options: []string{"active", "inactive", "pending"}},
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "status",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Options: []string{"active", "inactive", "pending"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "status",
|
||||
Operator: "equals",
|
||||
Value: "active",
|
||||
Fields: []ComponentField{
|
||||
{Name: "last_login", Type: "datetime"},
|
||||
{Name: "permissions", Type: "multiselect"},
|
||||
},
|
||||
Buttons: []ComponentButton{
|
||||
{
|
||||
Name: "deactivate",
|
||||
Label: "Deactivate User",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "warning"}},
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "status",
|
||||
Operator: "equals",
|
||||
Value: "active",
|
||||
Fields: []ComponentField{
|
||||
{Name: "last_login", Type: "datetime"},
|
||||
{Name: "permissions", Type: "multiselect"},
|
||||
},
|
||||
Buttons: []ComponentButton{
|
||||
{
|
||||
Name: "deactivate",
|
||||
Label: "Deactivate User",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "warning"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "status",
|
||||
Operator: "not_equals",
|
||||
Value: "active",
|
||||
Fields: []ComponentField{
|
||||
{
|
||||
Name: "reason",
|
||||
Type: "textarea",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Placeholder: stringPtr("Reason for status")},
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "status",
|
||||
Operator: "not_equals",
|
||||
Value: "active",
|
||||
Fields: []ComponentField{
|
||||
{
|
||||
Name: "reason",
|
||||
Type: "textarea",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Placeholder: stringPtr("Reason for status")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Buttons: []ComponentButton{
|
||||
{
|
||||
Name: "activate",
|
||||
Label: "Activate User",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "success"}},
|
||||
Buttons: []ComponentButton{
|
||||
{
|
||||
Name: "activate",
|
||||
Label: "Activate User",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "success"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "status",
|
||||
Operator: "contains",
|
||||
Value: "pending",
|
||||
Fields: []ComponentField{
|
||||
{Name: "approval_date", Type: "date"},
|
||||
},
|
||||
Buttons: []ComponentButton{
|
||||
{
|
||||
Name: "approve",
|
||||
Label: "Approve",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "primary"}},
|
||||
},
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "status",
|
||||
Operator: "contains",
|
||||
Value: "pending",
|
||||
Fields: []ComponentField{
|
||||
{Name: "approval_date", Type: "date"},
|
||||
},
|
||||
{
|
||||
Name: "reject",
|
||||
Label: "Reject",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "danger"}},
|
||||
Buttons: []ComponentButton{
|
||||
{
|
||||
Name: "approve",
|
||||
Label: "Approve",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "primary"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "reject",
|
||||
Label: "Reject",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "danger"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -203,104 +313,106 @@ func TestParseAdvancedUIFeatures(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Components: []Component{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Type: "form",
|
||||
Entity: stringPtr("Product"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Product Name")},
|
||||
{Placeholder: stringPtr("Enter product name")},
|
||||
{Required: true},
|
||||
{Default: stringPtr("New Product")},
|
||||
{Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
|
||||
{Size: stringPtr("large")},
|
||||
{Display: stringPtr("block")},
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("Product"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Product Name")},
|
||||
{Placeholder: stringPtr("Enter product name")},
|
||||
{Required: true},
|
||||
{Default: stringPtr("New Product")},
|
||||
{Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
|
||||
{Size: stringPtr("large")},
|
||||
{Display: stringPtr("block")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "price",
|
||||
Type: "number",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Price ($)")},
|
||||
{Format: stringPtr("currency")},
|
||||
{Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
|
||||
{Validation: &ComponentValidation{Type: "max", Value: stringPtr("10000")}},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "price",
|
||||
Type: "number",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Price ($)")},
|
||||
{Format: stringPtr("currency")},
|
||||
{Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
|
||||
{Validation: &ComponentValidation{Type: "max", Value: stringPtr("10000")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "category",
|
||||
Type: "autocomplete",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Category")},
|
||||
{Placeholder: stringPtr("Start typing...")},
|
||||
{Relates: &FieldRelation{Type: "Category"}},
|
||||
{Searchable: true},
|
||||
{Source: stringPtr("categories/search")},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "category",
|
||||
Type: "autocomplete",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Category")},
|
||||
{Placeholder: stringPtr("Start typing...")},
|
||||
{Relates: &FieldRelation{Type: "Category"}},
|
||||
{Searchable: true},
|
||||
{Source: stringPtr("categories/search")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "tags",
|
||||
Type: "multiselect",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Tags")},
|
||||
{Options: []string{"electronics", "clothing", "books", "home"}},
|
||||
{Source: stringPtr("tags/popular")},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "tags",
|
||||
Type: "multiselect",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Tags")},
|
||||
{Options: []string{"electronics", "clothing", "books", "home"}},
|
||||
{Source: stringPtr("tags/popular")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "description",
|
||||
Type: "richtext",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Description")},
|
||||
{Rows: intPtr(10)},
|
||||
{Placeholder: stringPtr("Describe your product...")},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "description",
|
||||
Type: "richtext",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Description")},
|
||||
{Rows: intPtr(10)},
|
||||
{Placeholder: stringPtr("Describe your product...")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "thumbnail",
|
||||
Type: "image",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Product Image")},
|
||||
{Accept: stringPtr("image/jpeg,image/png")},
|
||||
{Thumbnail: true},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "thumbnail",
|
||||
Type: "image",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Product Image")},
|
||||
{Accept: stringPtr("image/jpeg,image/png")},
|
||||
{Thumbnail: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "featured",
|
||||
Type: "checkbox",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Featured Product")},
|
||||
{Default: stringPtr("false")},
|
||||
{Value: stringPtr("true")},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "featured",
|
||||
Type: "checkbox",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Featured Product")},
|
||||
{Default: stringPtr("false")},
|
||||
{Value: stringPtr("true")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "availability",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Availability")},
|
||||
{Options: []string{"in_stock", "out_of_stock", "pre_order"}},
|
||||
{Default: stringPtr("in_stock")},
|
||||
{Sortable: true},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "availability",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Availability")},
|
||||
{Options: []string{"in_stock", "out_of_stock", "pre_order"}},
|
||||
{Default: stringPtr("in_stock")},
|
||||
{Sortable: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -332,76 +444,78 @@ func TestParseAdvancedUIFeatures(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Components: []Component{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Type: "form",
|
||||
Entity: stringPtr("Order"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "status",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Options: []string{"draft", "submitted", "approved"}},
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("Order"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "status",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Options: []string{"draft", "submitted", "approved"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "save",
|
||||
Label: "Save Draft",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "secondary"}},
|
||||
{Icon: &ComponentButtonIcon{Value: "save"}},
|
||||
{Position: &ComponentButtonPosition{Value: "left"}},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "save",
|
||||
Label: "Save Draft",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "secondary"}},
|
||||
{Icon: &ComponentButtonIcon{Value: "save"}},
|
||||
{Position: &ComponentButtonPosition{Value: "left"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "submit",
|
||||
Label: "Submit Order",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "primary"}},
|
||||
{Icon: &ComponentButtonIcon{Value: "send"}},
|
||||
{Loading: &ComponentButtonLoading{Value: "Submitting..."}},
|
||||
{Confirm: &ComponentButtonConfirm{Value: "Submit this order?"}},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "submit",
|
||||
Label: "Submit Order",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "primary"}},
|
||||
{Icon: &ComponentButtonIcon{Value: "send"}},
|
||||
{Loading: &ComponentButtonLoading{Value: "Submitting..."}},
|
||||
{Confirm: &ComponentButtonConfirm{Value: "Submit this order?"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "approve",
|
||||
Label: "Approve",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "success"}},
|
||||
{Loading: &ComponentButtonLoading{Value: "Approving..."}},
|
||||
{Disabled: &ComponentButtonDisabled{Value: "status"}},
|
||||
{Confirm: &ComponentButtonConfirm{Value: "Approve this order?"}},
|
||||
{Target: &ComponentButtonTarget{Value: "approval_modal"}},
|
||||
{Via: &ComponentButtonVia{Value: "api/orders/approve"}},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "approve",
|
||||
Label: "Approve",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "success"}},
|
||||
{Loading: &ComponentButtonLoading{Value: "Approving..."}},
|
||||
{Disabled: &ComponentButtonDisabled{Value: "status"}},
|
||||
{Confirm: &ComponentButtonConfirm{Value: "Approve this order?"}},
|
||||
{Target: &ComponentButtonTarget{Value: "approval_modal"}},
|
||||
{Via: &ComponentButtonVia{Value: "api/orders/approve"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "reject",
|
||||
Label: "Reject",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "danger"}},
|
||||
{Icon: &ComponentButtonIcon{Value: "x"}},
|
||||
{Confirm: &ComponentButtonConfirm{Value: "Are you sure you want to reject this order?"}},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "reject",
|
||||
Label: "Reject",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "danger"}},
|
||||
{Icon: &ComponentButtonIcon{Value: "x"}},
|
||||
{Confirm: &ComponentButtonConfirm{Value: "Are you sure you want to reject this order?"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "print",
|
||||
Label: "Print",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "outline"}},
|
||||
{Icon: &ComponentButtonIcon{Value: "printer"}},
|
||||
{Position: &ComponentButtonPosition{Value: "right"}},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "print",
|
||||
Label: "Print",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "outline"}},
|
||||
{Icon: &ComponentButtonIcon{Value: "printer"}},
|
||||
{Position: &ComponentButtonPosition{Value: "right"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -482,12 +596,12 @@ func TestParseFieldValidationTypes(t *testing.T) {
|
||||
}
|
||||
|
||||
page := got.Definitions[0].Page
|
||||
if len(page.Components) != 1 || len(page.Components[0].Elements) != 1 {
|
||||
if len(page.Elements) != 1 || page.Elements[0].Component == nil || len(page.Elements[0].Component.Elements) != 1 {
|
||||
t.Errorf("ParseInput() failed to parse component for validation %s", vt.validation)
|
||||
return
|
||||
}
|
||||
|
||||
element := page.Components[0].Elements[0]
|
||||
element := page.Elements[0].Component.Elements[0]
|
||||
if element.Field == nil || len(element.Field.Attributes) != 1 {
|
||||
t.Errorf("ParseInput() failed to parse field attributes for validation %s", vt.validation)
|
||||
return
|
||||
@ -527,7 +641,7 @@ func TestParseConditionalOperators(t *testing.T) {
|
||||
|
||||
// Verify the when condition was parsed correctly
|
||||
page := got.Definitions[0].Page
|
||||
component := page.Components[0]
|
||||
component := page.Elements[0].Component
|
||||
whenElement := component.Elements[1].When
|
||||
|
||||
if whenElement == nil || whenElement.Operator != op {
|
||||
|
Reference in New Issue
Block a user