allow for sections and components in any order on pages
This commit is contained in:
@ -96,7 +96,7 @@ func main() {
|
||||
for _, def := range ast.Definitions {
|
||||
if def.Page != nil {
|
||||
pageCount++
|
||||
totalContent := len(def.Page.Meta) + len(def.Page.Sections) + len(def.Page.Components)
|
||||
totalContent := len(def.Page.Meta) + len(def.Page.Elements)
|
||||
if totalContent > 0 {
|
||||
fmt.Printf(" ✓ Page '%s' has %d content items (block syntax working)\n", def.Page.Name, totalContent)
|
||||
}
|
||||
@ -110,9 +110,11 @@ func main() {
|
||||
var totalSections, nestedSections int
|
||||
for _, def := range ast.Definitions {
|
||||
if def.Page != nil {
|
||||
totalSections += len(def.Page.Sections)
|
||||
for _, section := range def.Page.Sections {
|
||||
nestedSections += countNestedSections(section)
|
||||
for _, element := range def.Page.Elements {
|
||||
if element.Section != nil {
|
||||
totalSections++
|
||||
nestedSections += countNestedSections(*element.Section)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
{{$relativePrefix := relativePrefix .Page.Path}}{{range .AST.Definitions}}{{if .Entity}}import { {{.Entity.Name}} } from '{{$relativePrefix}}types/{{.Entity.Name}}';
|
||||
{{end}}{{end}}
|
||||
{{range .Page.Sections}}import {{.Name | title}}Section from '{{$relativePrefix}}components/sections/{{.Name | title}}Section';
|
||||
{{end}}{{range .Page.Components}}import {{.Type | title}}Component from '{{$relativePrefix}}components/{{.Type | title}}Component';
|
||||
{{end}}
|
||||
{{range .Page.Elements}}{{if .Section}}import {{.Section.Name | title}}Section from '{{$relativePrefix}}components/sections/{{.Section.Name | title}}Section';
|
||||
{{end}}{{if .Component}}import {{.Component.Type | title}}Component from '{{$relativePrefix}}components/{{.Component.Type | title}}Component';
|
||||
{{end}}{{end}}
|
||||
|
||||
export default function {{.Page.Name}}Page() {
|
||||
const navigate = useNavigate();
|
||||
@ -53,12 +53,13 @@ export default function {{.Page.Name}}Page() {
|
||||
{{if .Page.Title}}{{.Page.Title | derefString}}{{else}}{{.Page.Name}}{{end}}
|
||||
</h1>
|
||||
|
||||
{{range .Page.Sections}}
|
||||
<{{.Name | title}}Section />
|
||||
{{range .Page.Elements}}
|
||||
{{if .Section}}
|
||||
<{{.Section.Name | title}}Section />
|
||||
{{end}}
|
||||
{{if .Component}}
|
||||
<{{.Component.Type | title}}Component />
|
||||
{{end}}
|
||||
|
||||
{{range .Page.Components}}
|
||||
<{{.Type | title}}Component />
|
||||
{{end}}
|
||||
</div>
|
||||
</main>
|
||||
|
@ -2,9 +2,9 @@ import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
{{$relativePrefix := relativePrefix .Page.Path}}{{range .AST.Definitions}}{{if .Entity}}import { {{.Entity.Name}} } from '{{$relativePrefix}}types/{{.Entity.Name}}';
|
||||
{{end}}{{end}}
|
||||
{{range .Page.Sections}}import {{.Name | title}}Section from '{{$relativePrefix}}components/sections/{{.Name | title}}Section';
|
||||
{{end}}{{range .Page.Components}}import {{.Type | title}}Component from '{{$relativePrefix}}components/{{.Type | title}}Component';
|
||||
{{end}}
|
||||
{{range .Page.Elements}}{{if .Section}}import {{.Section.Name | title}}Section from '{{$relativePrefix}}components/sections/{{.Section.Name | title}}Section';
|
||||
{{end}}{{if .Component}}import {{.Component.Type | title}}Component from '{{$relativePrefix}}components/{{.Component.Type | title}}Component';
|
||||
{{end}}{{end}}
|
||||
|
||||
export default function {{.Page.Name}}Page() {
|
||||
return (
|
||||
@ -36,12 +36,13 @@ export default function {{.Page.Name}}Page() {
|
||||
{{if .Page.Title}}{{.Page.Title | derefString}}{{else}}{{.Page.Name}}{{end}}
|
||||
</h1>
|
||||
|
||||
{{range .Page.Sections}}
|
||||
<{{.Name | title}}Section />
|
||||
{{range .Page.Elements}}
|
||||
{{if .Section}}
|
||||
<{{.Section.Name | title}}Section />
|
||||
{{end}}
|
||||
{{if .Component}}
|
||||
<{{.Component.Type | title}}Component />
|
||||
{{end}}
|
||||
|
||||
{{range .Page.Components}}
|
||||
<{{.Type | title}}Component />
|
||||
{{end}}
|
||||
</div>
|
||||
</main>
|
||||
|
@ -117,22 +117,22 @@ func (hi *HTMLInterpreter) generatePageHTML(page *lang.Page) (string, error) {
|
||||
html.WriteString(" <div class=\"container\">\n")
|
||||
html.WriteString(fmt.Sprintf(" <h1>%s</h1>\n", hi.escapeHTML(title)))
|
||||
|
||||
// Generate sections
|
||||
for _, section := range page.Sections {
|
||||
sectionHTML, err := hi.generateSectionHTML(§ion, 2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
// Generate page elements
|
||||
for _, element := range page.Elements {
|
||||
if element.Section != nil {
|
||||
sectionHTML, err := hi.generateSectionHTML(element.Section, 2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
html.WriteString(sectionHTML)
|
||||
}
|
||||
html.WriteString(sectionHTML)
|
||||
}
|
||||
|
||||
// Generate direct components
|
||||
for _, component := range page.Components {
|
||||
componentHTML, err := hi.generateComponentHTML(&component, 2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
if element.Component != nil {
|
||||
componentHTML, err := hi.generateComponentHTML(element.Component, 2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
html.WriteString(componentHTML)
|
||||
}
|
||||
html.WriteString(componentHTML)
|
||||
}
|
||||
|
||||
html.WriteString(" </div>\n")
|
||||
|
23
lang/lang.go
23
lang/lang.go
@ -122,15 +122,20 @@ type ResponseSpec struct {
|
||||
|
||||
// Page Enhanced Page definitions with unified section model
|
||||
type Page struct {
|
||||
Name string `parser:"'page' @Ident"`
|
||||
Path string `parser:"'at' @String"`
|
||||
Layout string `parser:"'layout' @Ident"`
|
||||
Title *string `parser:"('title' @String)?"`
|
||||
Description *string `parser:"('desc' @String)?"`
|
||||
Auth bool `parser:"@'auth'?"`
|
||||
Meta []MetaTag `parser:"('{' @@*"` // Block-delimited content
|
||||
Sections []Section `parser:"@@*"` // Unified sections replace containers/tabs/panels/modals
|
||||
Components []Component `parser:"@@* '}')?"` // Direct components within the block
|
||||
Name string `parser:"'page' @Ident"`
|
||||
Path string `parser:"'at' @String"`
|
||||
Layout string `parser:"'layout' @Ident"`
|
||||
Title *string `parser:"('title' @String)?"`
|
||||
Description *string `parser:"('desc' @String)?"`
|
||||
Auth bool `parser:"@'auth'?"`
|
||||
Meta []MetaTag `parser:"('{' @@*"` // Block-delimited content
|
||||
Elements []PageElement `parser:"@@* '}')?"` // Unified elements allowing any order
|
||||
}
|
||||
|
||||
// PageElement Unified element type for pages allowing sections and components in any order
|
||||
type PageElement struct {
|
||||
Section *Section `parser:"@@"`
|
||||
Component *Component `parser:"| @@"`
|
||||
}
|
||||
|
||||
// MetaTag Meta tags for SEO
|
||||
|
@ -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 {
|
||||
|
@ -23,10 +23,12 @@ func TestParseComponentDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Components: []Component{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Type: "table",
|
||||
Entity: stringPtr("User"),
|
||||
Component: &Component{
|
||||
Type: "table",
|
||||
Entity: stringPtr("User"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -52,58 +54,60 @@ func TestParseComponentDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Components: []Component{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Full Name")},
|
||||
{Placeholder: stringPtr("Enter your name")},
|
||||
{Required: true},
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Full Name")},
|
||||
{Placeholder: stringPtr("Enter your name")},
|
||||
{Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "email",
|
||||
Type: "email",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Email Address")},
|
||||
{Required: true},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "email",
|
||||
Type: "email",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Label: stringPtr("Email Address")},
|
||||
{Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "bio",
|
||||
Type: "textarea",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Rows: intPtr(5)},
|
||||
{Placeholder: stringPtr("Tell us about yourself")},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "bio",
|
||||
Type: "textarea",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Rows: intPtr(5)},
|
||||
{Placeholder: stringPtr("Tell us about yourself")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "avatar",
|
||||
Type: "file",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Accept: stringPtr("image/*")},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "avatar",
|
||||
Type: "file",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Accept: stringPtr("image/*")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "role",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Options: []string{"admin", "user", "guest"}},
|
||||
{Default: stringPtr("user")},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "role",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Options: []string{"admin", "user", "guest"}},
|
||||
{Default: stringPtr("user")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -135,70 +139,72 @@ func TestParseComponentDefinitions(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{
|
||||
{Required: true},
|
||||
{Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("Product"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
{Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "price",
|
||||
Type: "number",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Format: stringPtr("currency")},
|
||||
{Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "price",
|
||||
Type: "number",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Format: stringPtr("currency")},
|
||||
{Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "category",
|
||||
Type: "autocomplete",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Relates: &FieldRelation{Type: "Category"}},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "category",
|
||||
Type: "autocomplete",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Relates: &FieldRelation{Type: "Category"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "tags",
|
||||
Type: "multiselect",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Source: stringPtr("tags/popular")},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "tags",
|
||||
Type: "multiselect",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Source: stringPtr("tags/popular")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "description",
|
||||
Type: "richtext",
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "featured",
|
||||
Type: "checkbox",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Default: stringPtr("false")},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "description",
|
||||
Type: "richtext",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "thumbnail",
|
||||
Type: "image",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Thumbnail: true},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "featured",
|
||||
Type: "checkbox",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Default: stringPtr("false")},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "thumbnail",
|
||||
Type: "image",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Thumbnail: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -227,44 +233,46 @@ func TestParseComponentDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Components: []Component{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "save",
|
||||
Label: "Save User",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "primary"}},
|
||||
{Icon: &ComponentButtonIcon{Value: "save"}},
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "cancel",
|
||||
Label: "Cancel",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "secondary"}},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "save",
|
||||
Label: "Save User",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "primary"}},
|
||||
{Icon: &ComponentButtonIcon{Value: "save"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "delete",
|
||||
Label: "Delete",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "danger"}},
|
||||
{Confirm: &ComponentButtonConfirm{Value: "Are you sure?"}},
|
||||
{Disabled: &ComponentButtonDisabled{Value: "is_protected"}},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "cancel",
|
||||
Label: "Cancel",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "secondary"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "delete",
|
||||
Label: "Delete",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "danger"}},
|
||||
{Confirm: &ComponentButtonConfirm{Value: "Are you sure?"}},
|
||||
{Disabled: &ComponentButtonDisabled{Value: "is_protected"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -298,55 +306,57 @@ func TestParseComponentDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Components: []Component{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "account_type",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Options: []string{"personal", "business"}},
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "account_type",
|
||||
Type: "select",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Options: []string{"personal", "business"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "account_type",
|
||||
Operator: "equals",
|
||||
Value: "business",
|
||||
Fields: []ComponentField{
|
||||
{
|
||||
Name: "company_name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
{
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tax_id",
|
||||
Type: "text",
|
||||
},
|
||||
},
|
||||
Buttons: []ComponentButton{
|
||||
{
|
||||
Name: "verify_business",
|
||||
Label: "Verify Business",
|
||||
Buttons: []ComponentButton{
|
||||
{
|
||||
Name: "verify_business",
|
||||
Label: "Verify Business",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "account_type",
|
||||
Operator: "equals",
|
||||
Value: "personal",
|
||||
Fields: []ComponentField{
|
||||
{
|
||||
Name: "date_of_birth",
|
||||
Type: "date",
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "account_type",
|
||||
Operator: "equals",
|
||||
Value: "personal",
|
||||
Fields: []ComponentField{
|
||||
{
|
||||
Name: "date_of_birth",
|
||||
Type: "date",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -383,36 +393,38 @@ func TestParseComponentDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Components: []Component{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Type: "dashboard",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "stats",
|
||||
Type: stringPtr("container"),
|
||||
Class: stringPtr("stats-grid"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "metric",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "total_users",
|
||||
Type: "display",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Value: stringPtr("1,234")},
|
||||
Component: &Component{
|
||||
Type: "dashboard",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "stats",
|
||||
Type: stringPtr("container"),
|
||||
Class: stringPtr("stats-grid"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "metric",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "total_users",
|
||||
Type: "display",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Value: stringPtr("1,234")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "revenue",
|
||||
Type: "display",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Format: stringPtr("currency")},
|
||||
{Value: stringPtr("45,678")},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "revenue",
|
||||
Type: "display",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Format: stringPtr("currency")},
|
||||
{Value: stringPtr("45,678")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -421,20 +433,20 @@ func TestParseComponentDefinitions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "charts",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "chart",
|
||||
Entity: stringPtr("Analytics"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Attribute: &ComponentAttr{
|
||||
DataSource: stringPtr("analytics/monthly"),
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "charts",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "chart",
|
||||
Entity: stringPtr("Analytics"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Attribute: &ComponentAttr{
|
||||
DataSource: stringPtr("analytics/monthly"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -461,7 +473,7 @@ func TestParseComponentDefinitions(t *testing.T) {
|
||||
return
|
||||
}
|
||||
if !astEqual(got, tt.want) {
|
||||
t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
|
||||
t.Errorf("ParseInput() got = %+v, want %+v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -495,14 +507,20 @@ func TestParseComponentFieldTypes(t *testing.T) {
|
||||
}
|
||||
|
||||
page := got.Definitions[0].Page
|
||||
if len(page.Components) != 1 || len(page.Components[0].Elements) != 1 {
|
||||
if len(page.Elements) != 1 {
|
||||
t.Errorf("ParseInput() failed to parse component for field type %s", fieldType)
|
||||
return
|
||||
}
|
||||
|
||||
element := page.Components[0].Elements[0]
|
||||
if element.Field == nil || element.Field.Type != fieldType {
|
||||
t.Errorf("ParseInput() field type mismatch: got %v, want %s", element.Field, fieldType)
|
||||
element := page.Elements[0]
|
||||
if element.Component == nil || len(element.Component.Elements) != 1 {
|
||||
t.Errorf("ParseInput() failed to parse component for field type %s", fieldType)
|
||||
return
|
||||
}
|
||||
|
||||
fieldElement := element.Component.Elements[0]
|
||||
if fieldElement.Field == nil || fieldElement.Field.Type != fieldType {
|
||||
t.Errorf("ParseInput() field type mismatch: got %v, want %s", fieldElement.Field, fieldType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -12,14 +12,14 @@ func TestParsePageDefinitions(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic page with minimal fields",
|
||||
input: `page Dashboard at "/dashboard" layout main`,
|
||||
name: "basic page definition",
|
||||
input: `page Home at "/" layout main`,
|
||||
want: AST{
|
||||
Definitions: []Definition{
|
||||
{
|
||||
Page: &Page{
|
||||
Name: "Dashboard",
|
||||
Path: "/dashboard",
|
||||
Name: "Home",
|
||||
Path: "/",
|
||||
Layout: "main",
|
||||
},
|
||||
},
|
||||
@ -27,17 +27,17 @@ func TestParsePageDefinitions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "page with all optional fields",
|
||||
input: `page UserProfile at "/profile" layout main title "User Profile" desc "Manage user profile settings" auth`,
|
||||
name: "page with optional fields",
|
||||
input: `page Settings at "/settings" layout main title "User Settings" desc "Manage your account settings" auth`,
|
||||
want: AST{
|
||||
Definitions: []Definition{
|
||||
{
|
||||
Page: &Page{
|
||||
Name: "UserProfile",
|
||||
Path: "/profile",
|
||||
Name: "Settings",
|
||||
Path: "/settings",
|
||||
Layout: "main",
|
||||
Title: stringPtr("User Profile"),
|
||||
Description: stringPtr("Manage user profile settings"),
|
||||
Title: stringPtr("User Settings"),
|
||||
Description: stringPtr("Manage your account settings"),
|
||||
Auth: true,
|
||||
},
|
||||
},
|
||||
@ -46,20 +46,22 @@ func TestParsePageDefinitions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "page with meta tags",
|
||||
input: `page HomePage at "/" layout main {
|
||||
meta description "Welcome to our application"
|
||||
meta keywords "app, dashboard, management"
|
||||
input: `page Settings at "/settings" layout main {
|
||||
meta description "Settings page description"
|
||||
meta keywords "settings, user, account"
|
||||
meta author "My App"
|
||||
}`,
|
||||
want: AST{
|
||||
Definitions: []Definition{
|
||||
{
|
||||
Page: &Page{
|
||||
Name: "HomePage",
|
||||
Path: "/",
|
||||
Name: "Settings",
|
||||
Path: "/settings",
|
||||
Layout: "main",
|
||||
Meta: []MetaTag{
|
||||
{Name: "description", Content: "Welcome to our application"},
|
||||
{Name: "keywords", Content: "app, dashboard, management"},
|
||||
{Name: "description", Content: "Settings page description"},
|
||||
{Name: "keywords", Content: "settings, user, account"},
|
||||
{Name: "author", Content: "My App"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -67,19 +69,17 @@ func TestParsePageDefinitions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "page with nested sections",
|
||||
name: "page with sections",
|
||||
input: `page Settings at "/settings" layout main {
|
||||
section tabs type tab {
|
||||
section profile label "Profile" active {
|
||||
component form for User {
|
||||
field name type text
|
||||
}
|
||||
component form for User
|
||||
}
|
||||
|
||||
section security label "Security" {
|
||||
component form for User {
|
||||
field password type password
|
||||
}
|
||||
component form for Security
|
||||
}
|
||||
section notifications label "Notifications" {
|
||||
component toggle for NotificationSettings
|
||||
}
|
||||
}
|
||||
}`,
|
||||
@ -90,50 +90,50 @@ func TestParsePageDefinitions(t *testing.T) {
|
||||
Name: "Settings",
|
||||
Path: "/settings",
|
||||
Layout: "main",
|
||||
Sections: []Section{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Name: "tabs",
|
||||
Type: stringPtr("tab"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "profile",
|
||||
Label: stringPtr("Profile"),
|
||||
Active: true,
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
},
|
||||
},
|
||||
Section: &Section{
|
||||
Name: "tabs",
|
||||
Type: stringPtr("tab"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "profile",
|
||||
Label: stringPtr("Profile"),
|
||||
Active: true,
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "security",
|
||||
Label: stringPtr("Security"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "password",
|
||||
Type: "password",
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "security",
|
||||
Label: stringPtr("Security"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("Security"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "notifications",
|
||||
Label: stringPtr("Notifications"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "toggle",
|
||||
Entity: stringPtr("NotificationSettings"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -149,71 +149,124 @@ func TestParsePageDefinitions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "page with modal and panel sections",
|
||||
input: `page ProductList at "/products" layout main {
|
||||
section main type container {
|
||||
component table for Product
|
||||
name: "page with components",
|
||||
input: `page Dashboard at "/dashboard" layout main {
|
||||
component stats for Analytics {
|
||||
field total_users type display
|
||||
field revenue type display format "currency"
|
||||
}
|
||||
|
||||
section editModal type modal trigger "edit-product" {
|
||||
component form for Product {
|
||||
field name type text required
|
||||
button save label "Save Changes" style "primary"
|
||||
}
|
||||
}
|
||||
|
||||
section filters type panel position "left" {
|
||||
component form {
|
||||
field category type select
|
||||
field price_range type range
|
||||
}
|
||||
component chart for SalesData {
|
||||
data from "analytics/sales"
|
||||
}
|
||||
}`,
|
||||
want: AST{
|
||||
Definitions: []Definition{
|
||||
{
|
||||
Page: &Page{
|
||||
Name: "ProductList",
|
||||
Path: "/products",
|
||||
Name: "Dashboard",
|
||||
Path: "/dashboard",
|
||||
Layout: "main",
|
||||
Sections: []Section{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Name: "main",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "table",
|
||||
Entity: stringPtr("Product"),
|
||||
Component: &Component{
|
||||
Type: "stats",
|
||||
Entity: stringPtr("Analytics"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "total_users",
|
||||
Type: "display",
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "revenue",
|
||||
Type: "display",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Format: stringPtr("currency")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "editModal",
|
||||
Type: stringPtr("modal"),
|
||||
Trigger: stringPtr("edit-product"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("Product"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "save",
|
||||
Label: "Save Changes",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "primary"}},
|
||||
Component: &Component{
|
||||
Type: "chart",
|
||||
Entity: stringPtr("SalesData"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Attribute: &ComponentAttr{
|
||||
DataSource: stringPtr("analytics/sales"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "page with mixed sections and components",
|
||||
input: `page Home at "/" layout main {
|
||||
component hero for Banner {
|
||||
field title type display
|
||||
field subtitle type display
|
||||
}
|
||||
section content type container {
|
||||
component posts for Post {
|
||||
fields [title, excerpt, date]
|
||||
}
|
||||
}
|
||||
component newsletter for Subscription {
|
||||
field email type email required
|
||||
button subscribe label "Subscribe"
|
||||
}
|
||||
}`,
|
||||
want: AST{
|
||||
Definitions: []Definition{
|
||||
{
|
||||
Page: &Page{
|
||||
Name: "Home",
|
||||
Path: "/",
|
||||
Layout: "main",
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "hero",
|
||||
Entity: stringPtr("Banner"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "title",
|
||||
Type: "display",
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "subtitle",
|
||||
Type: "display",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "content",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "posts",
|
||||
Entity: stringPtr("Post"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Attribute: &ComponentAttr{
|
||||
Fields: []string{"title", "excerpt", "date"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -223,28 +276,25 @@ func TestParsePageDefinitions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "filters",
|
||||
Type: stringPtr("panel"),
|
||||
Position: stringPtr("left"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "category",
|
||||
Type: "select",
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "price_range",
|
||||
Type: "range",
|
||||
},
|
||||
Component: &Component{
|
||||
Type: "newsletter",
|
||||
Entity: stringPtr("Subscription"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "email",
|
||||
Type: "email",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "subscribe",
|
||||
Label: "Subscribe",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -264,7 +314,66 @@ func TestParsePageDefinitions(t *testing.T) {
|
||||
return
|
||||
}
|
||||
if !astEqual(got, tt.want) {
|
||||
t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
|
||||
t.Errorf("ParseInput() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePageLayouts(t *testing.T) {
|
||||
layouts := []string{"main", "admin", "public", "auth", "minimal", "dashboard"}
|
||||
|
||||
for _, layout := range layouts {
|
||||
t.Run("layout_"+layout, func(t *testing.T) {
|
||||
input := `page Test at "/test" layout ` + layout
|
||||
|
||||
got, err := ParseInput(input)
|
||||
if err != nil {
|
||||
t.Errorf("ParseInput() failed for layout %s: %v", layout, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
|
||||
t.Errorf("ParseInput() failed to parse page for layout %s", layout)
|
||||
return
|
||||
}
|
||||
|
||||
page := got.Definitions[0].Page
|
||||
if page.Layout != layout {
|
||||
t.Errorf("ParseInput() layout mismatch: got %s, want %s", page.Layout, layout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePagePaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{"root", "/"},
|
||||
{"simple", "/about"},
|
||||
{"nested", "/admin/users"},
|
||||
{"deep_nested", "/api/v1/users/profile"},
|
||||
{"with_params", "/users/:id"},
|
||||
{"with_multiple_params", "/users/:userId/posts/:postId"},
|
||||
{"with_query", "/search?q=:query"},
|
||||
{"with_extension", "/api/users.json"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := `page Test at "` + tt.path + `" layout main`
|
||||
|
||||
got, err := ParseInput(input)
|
||||
if err != nil {
|
||||
t.Errorf("ParseInput() failed for path %s: %v", tt.path, err)
|
||||
return
|
||||
}
|
||||
|
||||
page := got.Definitions[0].Page
|
||||
if page.Path != tt.path {
|
||||
t.Errorf("ParseInput() path mismatch: got %s, want %s", page.Path, tt.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -276,16 +385,26 @@ func TestParsePageErrors(t *testing.T) {
|
||||
input string
|
||||
}{
|
||||
{
|
||||
name: "missing layout",
|
||||
input: `page Dashboard at "/dashboard"`,
|
||||
name: "missing page name",
|
||||
input: `page at "/" layout main`,
|
||||
},
|
||||
{
|
||||
name: "missing path",
|
||||
input: `page Dashboard layout main`,
|
||||
input: `page Test layout main`,
|
||||
},
|
||||
{
|
||||
name: "missing layout",
|
||||
input: `page Test at "/"`,
|
||||
},
|
||||
{
|
||||
name: "invalid path format",
|
||||
input: `page Dashboard at dashboard layout main`,
|
||||
input: `page Test at /invalid layout main`,
|
||||
},
|
||||
{
|
||||
name: "unclosed page block",
|
||||
input: `page Test at "/" layout main {
|
||||
section test type container
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -23,10 +23,12 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Sections: []Section{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Name: "main",
|
||||
Type: stringPtr("container"),
|
||||
Section: &Section{
|
||||
Name: "main",
|
||||
Type: stringPtr("container"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -46,15 +48,57 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Sections: []Section{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Name: "sidebar",
|
||||
Type: stringPtr("panel"),
|
||||
Position: stringPtr("left"),
|
||||
Class: stringPtr("sidebar-nav"),
|
||||
Label: stringPtr("Navigation"),
|
||||
Trigger: stringPtr("toggle-sidebar"),
|
||||
Entity: stringPtr("User"),
|
||||
Section: &Section{
|
||||
Name: "sidebar",
|
||||
Type: stringPtr("panel"),
|
||||
Class: stringPtr("sidebar-nav"),
|
||||
Label: stringPtr("Navigation"),
|
||||
Trigger: stringPtr("toggle-sidebar"),
|
||||
Position: stringPtr("left"),
|
||||
Entity: stringPtr("User"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sections with separate attributes",
|
||||
input: `page Dashboard at "/dashboard" layout main {
|
||||
section content type container {
|
||||
data from "/api/data"
|
||||
style "padding: 20px"
|
||||
}
|
||||
}`,
|
||||
want: AST{
|
||||
Definitions: []Definition{
|
||||
{
|
||||
Page: &Page{
|
||||
Name: "Dashboard",
|
||||
Path: "/dashboard",
|
||||
Layout: "main",
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "content",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Attribute: &SectionAttribute{
|
||||
DataSource: stringPtr("/api/data"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Attribute: &SectionAttribute{
|
||||
Style: stringPtr("padding: 20px"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -78,28 +122,30 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Sections: []Section{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Name: "tabs",
|
||||
Type: stringPtr("tab"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "overview",
|
||||
Label: stringPtr("Overview"),
|
||||
Active: true,
|
||||
Section: &Section{
|
||||
Name: "tabs",
|
||||
Type: stringPtr("tab"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "overview",
|
||||
Label: stringPtr("Overview"),
|
||||
Active: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "details",
|
||||
Label: stringPtr("Details"),
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "details",
|
||||
Label: stringPtr("Details"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "settings",
|
||||
Label: stringPtr("Settings"),
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "settings",
|
||||
Label: stringPtr("Settings"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -129,50 +175,52 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Sections: []Section{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Name: "userModal",
|
||||
Type: stringPtr("modal"),
|
||||
Trigger: stringPtr("edit-user"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
Section: &Section{
|
||||
Name: "userModal",
|
||||
Type: stringPtr("modal"),
|
||||
Trigger: stringPtr("edit-user"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "email",
|
||||
Type: "email",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "email",
|
||||
Type: "email",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "save",
|
||||
Label: "Save Changes",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "primary"}},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "save",
|
||||
Label: "Save Changes",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "primary"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "cancel",
|
||||
Label: "Cancel",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "secondary"}},
|
||||
{
|
||||
Button: &ComponentButton{
|
||||
Name: "cancel",
|
||||
Label: "Cancel",
|
||||
Attributes: []ComponentButtonAttr{
|
||||
{Style: &ComponentButtonStyle{Value: "secondary"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -213,24 +261,26 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Sections: []Section{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Name: "masterDetail",
|
||||
Type: stringPtr("master"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "userList",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "table",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Attribute: &ComponentAttr{
|
||||
Fields: []string{"name", "email"},
|
||||
Section: &Section{
|
||||
Name: "masterDetail",
|
||||
Type: stringPtr("master"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "userList",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "table",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Attribute: &ComponentAttr{
|
||||
Fields: []string{"name", "email"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -238,35 +288,35 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "userDetail",
|
||||
Type: stringPtr("detail"),
|
||||
Trigger: stringPtr("user-selected"),
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "userDetail",
|
||||
Type: stringPtr("detail"),
|
||||
Trigger: stringPtr("user-selected"),
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("User"),
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "name",
|
||||
Type: "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "email",
|
||||
Type: "email",
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "email",
|
||||
Type: "email",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "bio",
|
||||
Type: "textarea",
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "bio",
|
||||
Type: "textarea",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -323,27 +373,29 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Sections: []Section{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Name: "mainLayout",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "header",
|
||||
Type: stringPtr("container"),
|
||||
Class: stringPtr("header"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "navbar",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "search",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Placeholder: stringPtr("Search...")},
|
||||
Section: &Section{
|
||||
Name: "mainLayout",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "header",
|
||||
Type: stringPtr("container"),
|
||||
Class: stringPtr("header"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "navbar",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "search",
|
||||
Type: "text",
|
||||
Attributes: []ComponentFieldAttribute{
|
||||
{Placeholder: stringPtr("Search...")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -352,26 +404,26 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "content",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "sidebar",
|
||||
Type: stringPtr("panel"),
|
||||
Position: stringPtr("left"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "menu",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "navigation",
|
||||
Type: "list",
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "content",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "sidebar",
|
||||
Type: stringPtr("panel"),
|
||||
Position: stringPtr("left"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "menu",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "navigation",
|
||||
Type: "list",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -379,31 +431,31 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "main",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "tabs",
|
||||
Type: stringPtr("tab"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "overview",
|
||||
Label: stringPtr("Overview"),
|
||||
Active: true,
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "dashboard",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "stats",
|
||||
Type: "metric",
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "main",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "tabs",
|
||||
Type: stringPtr("tab"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "overview",
|
||||
Label: stringPtr("Overview"),
|
||||
Active: true,
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "dashboard",
|
||||
Elements: []ComponentElement{
|
||||
{
|
||||
Field: &ComponentField{
|
||||
Name: "stats",
|
||||
Type: "metric",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -411,16 +463,16 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "reports",
|
||||
Label: stringPtr("Reports"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "table",
|
||||
Entity: stringPtr("Report"),
|
||||
{
|
||||
Section: &Section{
|
||||
Name: "reports",
|
||||
Label: stringPtr("Reports"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "table",
|
||||
Entity: stringPtr("Report"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -464,37 +516,39 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
Name: "Test",
|
||||
Path: "/test",
|
||||
Layout: "main",
|
||||
Sections: []Section{
|
||||
Elements: []PageElement{
|
||||
{
|
||||
Name: "adminPanel",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "user_role",
|
||||
Operator: "equals",
|
||||
Value: "admin",
|
||||
Sections: []Section{
|
||||
{
|
||||
Name: "userManagement",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "table",
|
||||
Entity: stringPtr("User"),
|
||||
Section: &Section{
|
||||
Name: "adminPanel",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
When: &WhenCondition{
|
||||
Field: "user_role",
|
||||
Operator: "equals",
|
||||
Value: "admin",
|
||||
Sections: []Section{
|
||||
{
|
||||
Name: "userManagement",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "table",
|
||||
Entity: stringPtr("User"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "systemSettings",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("Settings"),
|
||||
{
|
||||
Name: "systemSettings",
|
||||
Type: stringPtr("container"),
|
||||
Elements: []SectionElement{
|
||||
{
|
||||
Component: &Component{
|
||||
Type: "form",
|
||||
Entity: stringPtr("Settings"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -520,7 +574,7 @@ func TestParseSectionDefinitions(t *testing.T) {
|
||||
return
|
||||
}
|
||||
if !astEqual(got, tt.want) {
|
||||
t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
|
||||
t.Errorf("ParseInput() got = %+v, want %+v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -549,12 +603,12 @@ func TestParseSectionTypes(t *testing.T) {
|
||||
}
|
||||
|
||||
page := got.Definitions[0].Page
|
||||
if len(page.Sections) != 1 {
|
||||
if len(page.Elements) != 1 || page.Elements[0].Section == nil {
|
||||
t.Errorf("ParseInput() failed to parse section for type %s", sectionType)
|
||||
return
|
||||
}
|
||||
|
||||
section := page.Sections[0]
|
||||
section := page.Elements[0].Section
|
||||
if section.Type == nil || *section.Type != sectionType {
|
||||
t.Errorf("ParseInput() section type mismatch: got %v, want %s", section.Type, sectionType)
|
||||
}
|
||||
|
@ -30,24 +30,13 @@ func pageEqual(got, want Page) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Compare sections (unified model)
|
||||
if len(got.Sections) != len(want.Sections) {
|
||||
// Compare elements (unified model)
|
||||
if len(got.Elements) != len(want.Elements) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, section := range got.Sections {
|
||||
if !sectionEqual(section, want.Sections[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Compare components
|
||||
if len(got.Components) != len(want.Components) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, component := range got.Components {
|
||||
if !componentEqual(component, want.Components[i]) {
|
||||
for i, element := range got.Elements {
|
||||
if !pageElementEqual(element, want.Elements[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -55,6 +44,28 @@ func pageEqual(got, want Page) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func pageElementEqual(got, want PageElement) bool {
|
||||
// Both should have either a Section or Component, but not both
|
||||
if (got.Section == nil) != (want.Section == nil) {
|
||||
return false
|
||||
}
|
||||
if (got.Component == nil) != (want.Component == nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare sections if present
|
||||
if got.Section != nil && want.Section != nil {
|
||||
return sectionEqual(*got.Section, *want.Section)
|
||||
}
|
||||
|
||||
// Compare components if present
|
||||
if got.Component != nil && want.Component != nil {
|
||||
return componentEqual(*got.Component, *want.Component)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func metaTagEqual(got, want MetaTag) bool {
|
||||
return got.Name == want.Name && got.Content == want.Content
|
||||
}
|
||||
|
@ -583,13 +583,19 @@ export default function {{.Page.Name}}Page() {
|
||||
{/* Protected content */}
|
||||
{{end}}
|
||||
|
||||
{{range .Page.Sections}}
|
||||
<section className="{{.Class | derefString}}">
|
||||
{{if .Label}}<h2>{{.Label | derefString}}</h2>{{end}}
|
||||
{{range .Components}}
|
||||
{/* Component: {{.Type}} */}
|
||||
{{range .Page.Elements}}
|
||||
{{if .Section}}
|
||||
<section className="{{.Section.Class | derefString}}">
|
||||
{{if .Section.Label}}<h2>{{.Section.Label | derefString}}</h2>{{end}}
|
||||
{{range .Section.Elements}}
|
||||
{{if .Component}}
|
||||
{/* Component: {{.Component.Type}} */}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</section>
|
||||
{{else if .Component}}
|
||||
{/* Component: {{.Component.Type}} */}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</Layout>
|
||||
|
Reference in New Issue
Block a user