diff --git a/lang/lang.go b/lang/lang.go index 382861c9..4db2377b 100644 --- a/lang/lang.go +++ b/lang/lang.go @@ -11,6 +11,7 @@ import ( func init() { lang.RegisterWidget("tables", "table") + lang.RegisterWidget("forms", "form") lang.RegisterWidget("charts", "chart") lang.RegisterWidget("kanban", "page") lang.RegisterWidget("screen", "page") diff --git a/widgets/action/action.go b/widgets/action/action.go new file mode 100644 index 00000000..5878a6ba --- /dev/null +++ b/widgets/action/action.go @@ -0,0 +1,148 @@ +package action + +import ( + "fmt" + + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/yao/widgets/hook" +) + +// Bind the process name +func (p *Process) Bind(name string) { + p.ProcessBind = name +} + +// NewProcess create a new process +func NewProcess(name string, p *Process) *Process { + if p == nil { + p = &Process{} + } + p.Name = name + return p +} + +// SetHandler set the handler +func (p *Process) SetHandler(handler Handler) *Process { + p.Handler = handler + return p +} + +// SetDefault set the default value +func (p *Process) SetDefault(defaults map[string]*Process) *Process { + + if defaultProcess, has := defaults[p.Name]; has { + + p.Name = defaultProcess.Name + + if p.Process == "" { + p.Process = defaultProcess.Process + } + + if p.Guard == "" { + p.Guard = defaultProcess.Guard + } + + if p.Default == nil { + p.Default = defaultProcess.Default + } + + // format defaults + if len(p.Default) != len(defaultProcess.Default) { + defauts := defaultProcess.Default + nums := len(p.Default) + if nums > len(defaultProcess.Default) { + nums = len(defaultProcess.Default) + } + for i := 0; i < nums; i++ { + defauts[i] = p.Default[i] + } + p.Default = defauts + } + } + + return p +} + +// WithBefore bind before hook +func (p *Process) WithBefore(before *hook.Before) *Process { + p.Before = before + return p +} + +// WithAfter bind after hook +func (p *Process) WithAfter(after *hook.After) *Process { + p.After = after + return p +} + +// Args get the process args +func (p *Process) Args(process *gou.Process) []interface{} { + process.ValidateArgNums(1) + args := p.Default + nums := len(process.Args[1:]) + if nums > len(args) { + nums = len(args) + } + + for i := 0; i < nums; i++ { + args[i] = p.deepMergeDefault(process.Args[i+1], args[i]) + } + return args +} + +// Exec exec the process +func (p *Process) Exec(process *gou.Process) (interface{}, error) { + if p.Handler == nil { + return nil, fmt.Errorf("%s handler does not set", p.Name) + } + return p.Handler(p, process) +} + +// MustExec exec the process +func (p *Process) MustExec(process *gou.Process) interface{} { + res, err := p.Exec(process) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + return res +} + +// deepMergeDefault deep merge args +func (p *Process) deepMergeDefault(value interface{}, defaults interface{}) interface{} { + + if value == nil { + return defaults + } + + switch defaults.(type) { + + case map[string]interface{}: + defaultMap := defaults.(map[string]interface{}) + valueMap := any.Of(value).MapStr() + for key, v := range defaultMap { + valueMap[key] = p.deepMergeDefault(valueMap[key], v) + } + return valueMap + + case []interface{}: + defaultArr := defaults.([]interface{}) + valueArr := any.Of(value).CArray() + + // pad + nums := len(defaultArr) - len(valueArr) + for i := 0; i < nums; i++ { + valueArr = append(valueArr, nil) + } + + // set default + for idx, v := range defaultArr { + valueArr[idx] = p.deepMergeDefault(valueArr[idx], v) + } + + return valueArr + } + + return value +} diff --git a/widgets/action/types.go b/widgets/action/types.go new file mode 100644 index 00000000..6050e182 --- /dev/null +++ b/widgets/action/types.go @@ -0,0 +1,22 @@ +package action + +import ( + "github.com/yaoapp/gou" + "github.com/yaoapp/yao/widgets/hook" +) + +// Process action.search ... +type Process struct { + Name string `json:"-"` + Process string `json:"process,omitempty"` + ProcessBind string `json:"bind,omitempty"` + Guard string `json:"guard,omitempty"` + Default []interface{} `json:"default,omitempty"` + Disable bool `json:"disable,omitempty"` + Before *hook.Before `json:"-"` + After *hook.After `json:"-"` + Handler Handler `json:"-"` +} + +// Handler action handler +type Handler func(p *Process, process *gou.Process) (interface{}, error) diff --git a/widgets/app/app.go b/widgets/app/app.go index 288bff00..59c70a73 100644 --- a/widgets/app/app.go +++ b/widgets/app/app.go @@ -29,6 +29,15 @@ import ( // Setting the application setting var Setting *DSL +// LoadAndExport load app +func LoadAndExport(cfg config.Config) error { + err := Load(cfg) + if err != nil { + return err + } + return Export() +} + // Load the app DSL func Load(cfg config.Config) error { diff --git a/widgets/component/types.go b/widgets/component/types.go index 14654f92..76afcadc 100644 --- a/widgets/component/types.go +++ b/widgets/component/types.go @@ -8,6 +8,12 @@ type DSL struct { Props PropsDSL `json:"props,omitempty"` } +// Actions the actions +type Actions []ActionDSL + +// Instances the Instances +type Instances []InstanceDSL + // InstanceDSL the component instance DSL type InstanceDSL struct { Name string `json:"name,omitempty"` diff --git a/widgets/field/field.go b/widgets/field/field.go new file mode 100644 index 00000000..bed7e637 --- /dev/null +++ b/widgets/field/field.go @@ -0,0 +1,73 @@ +package field + +import ( + "github.com/yaoapp/yao/widgets/component" +) + +// CPropsMerge merge the Filters cloud props +func (filters Filters) CPropsMerge(cloudProps map[string]component.CloudPropsDSL, getXpath func(name string, filter FilterDSL) (xpath string)) error { + + for name, filter := range filters { + if filter.Edit != nil && filter.Edit.Props != nil { + xpath := getXpath(name, filter) + cProps, err := filter.Edit.Props.CloudProps(xpath) + if err != nil { + return err + } + mergeCProps(cloudProps, cProps) + } + } + + return nil +} + +// CPropsMerge merge the Columns cloud props +func (columns Columns) CPropsMerge(cloudProps map[string]component.CloudPropsDSL, getXpath func(name string, kind string, column ColumnDSL) (xpath string)) error { + + for name, column := range columns { + + if column.Edit != nil && column.Edit.Props != nil { + xpath := getXpath(name, "edit", column) + cProps, err := column.Edit.Props.CloudProps(xpath) + if err != nil { + return err + } + mergeCProps(cloudProps, cProps) + } + + if column.View != nil && column.View.Props != nil { + xpath := getXpath(name, "view", column) + cProps, err := column.View.Props.CloudProps(xpath) + if err != nil { + return err + } + mergeCProps(cloudProps, cProps) + } + } + + return nil +} + +// ComputeFieldsMerge merge the compute fields +func (columns Columns) ComputeFieldsMerge(computeInFields map[string]string, computeOutFields map[string]string) { + for name, column := range columns { + + // Compute In + if column.In != "" { + computeInFields[column.Bind] = column.In + computeInFields[name] = column.In + } + + // Compute Out + if column.Out != "" { + computeOutFields[column.Bind] = column.Out + computeOutFields[name] = column.Out + } + } +} + +func mergeCProps(cloudProps map[string]component.CloudPropsDSL, cProps map[string]component.CloudPropsDSL) { + for k, v := range cProps { + cloudProps[k] = v + } +} diff --git a/widgets/field/types.go b/widgets/field/types.go new file mode 100644 index 00000000..25014920 --- /dev/null +++ b/widgets/field/types.go @@ -0,0 +1,30 @@ +package field + +import "github.com/yaoapp/yao/widgets/component" + +// Filters the filters DSL +type Filters map[string]FilterDSL + +// Columns the columns DSL +type Columns map[string]ColumnDSL + +// ComputeFields the Compute filelds +type ComputeFields map[string]string + +// CloudProps the cloud props +type CloudProps map[string]component.CloudPropsDSL + +// ColumnDSL the field column dsl +type ColumnDSL struct { + Bind string `json:"bind,omitempty"` + In string `json:"in,omitempty"` + Out string `json:"out,omitempty"` + View *component.DSL `json:"view,omitempty"` + Edit *component.DSL `json:"edit,omitempty"` +} + +// FilterDSL the field filter dsl +type FilterDSL struct { + Bind string `json:"bind,omitempty"` + Edit *component.DSL `json:"edit,omitempty"` +} diff --git a/widgets/form/action.go b/widgets/form/action.go new file mode 100644 index 00000000..6f93ed08 --- /dev/null +++ b/widgets/form/action.go @@ -0,0 +1,201 @@ +package form + +import ( + "fmt" + + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/kun/maps" + "github.com/yaoapp/yao/widgets/action" +) + +var processActionDefaults = map[string]*action.Process{ + + "Setting": { + Name: "yao.form.Setting", + Process: "yao.form.Xgen", + Default: []interface{}{nil}, + }, + "Component": { + Name: "yao.form.Component", + Default: []interface{}{nil, nil, nil}, + }, + "Find": { + Name: "yao.form.Find", + Default: []interface{}{nil, nil}, + }, + "Save": { + Name: "yao.form.Save", + Default: []interface{}{nil}, + }, + "Create": { + Name: "yao.form.Create", + Default: []interface{}{nil}, + }, + "Update": { + Name: "yao.form.Update", + Default: []interface{}{nil, nil}, + }, + "Delete": { + Name: "yao.table.Delete", + Default: []interface{}{nil}, + }, +} + +// SetDefaultProcess set the default value of action +func (act *ActionDSL) SetDefaultProcess() { + + act.Setting = action.NewProcess("Setting", act.Setting). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Component = action.NewProcess("Component", act.Component). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Find = action.NewProcess("Find", act.Find). + WithBefore(act.BeforeFind). + WithAfter(act.AfterFind). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Save = action.NewProcess("Save", act.Save). + WithBefore(act.BeforeSave).WithAfter(act.AfterSave). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Create = action.NewProcess("Create", act.Create). + WithBefore(act.BeforeCreate).WithAfter(act.AfterCreate). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Update = action.NewProcess("Update", act.Update). + WithBefore(act.BeforeUpdate).WithAfter(act.AfterUpdate). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Delete = action.NewProcess("Delete", act.Delete). + WithBefore(act.BeforeDelete).WithAfter(act.AfterDelete). + SetDefault(processActionDefaults). + SetHandler(processHandler) + +} + +// BindModel bind model +func (act *ActionDSL) BindModel(m *gou.Model) { + + name := m.ID + act.Find.Bind(fmt.Sprintf("models.%s.Find", name)) + act.Save.Bind(fmt.Sprintf("models.%s.Save", name)) + act.Create.Bind(fmt.Sprintf("models.%s.Create", name)) + act.Update.Bind(fmt.Sprintf("models.%s.Update", name)) + act.Delete.Bind(fmt.Sprintf("models.%s.Delete", name)) + + // bind options + if act.Bind.Option != nil { + act.Find.Default[1] = act.Bind.Option + } +} + +func processHandler(p *action.Process, process *gou.Process) (interface{}, error) { + + form, err := Get(process) + if err != nil { + return nil, err + } + args := p.Args(process) + + // Process + name := p.Process + if name == "" { + name = p.ProcessBind + } + + if name == "" { + log.Error("[form] %s %s process is required", form.ID, p.Name) + return nil, fmt.Errorf("[form] %s %s process is required", form.ID, p.Name) + } + + // Before Hook + if p.Before != nil { + log.Trace("[form] %s %s before: exec(%v)", form.ID, p.Name, args) + newArgs, err := p.Before.Exec(args, process.Sid, process.Global) + if err != nil { + log.Error("[form] %s %s before: %s", form.ID, p.Name, err.Error()) + } else { + log.Trace("[form] %s %s before: args:%v", form.ID, p.Name, args) + args = newArgs + } + } + + // Compute In + switch p.Name { + case "yao.form.Save", "yao.form.Create": + switch args[0].(type) { + case map[string]interface{}: + data := args[0].(map[string]interface{}) + err := form.computeSave(process, data) + if err != nil { + log.Error("[form] %s %s -> %s %s", form.ID, p.Name, name, err.Error()) + } + args[0] = data + } + break + + case "yao.form.Update": + switch args[1].(type) { + case map[string]interface{}: + data := args[1].(map[string]interface{}) + err := form.computeSave(process, data) + if err != nil { + log.Error("[form] %s %s -> %s %s", form.ID, p.Name, name, err.Error()) + } + args[1] = data + } + break + } + + // Execute Process + act, err := gou.ProcessOf(name, args...) + if err != nil { + log.Error("[form] %s %s -> %s %s", form.ID, p.Name, name, err.Error()) + return nil, fmt.Errorf("[form] %s %s -> %s %s", form.ID, p.Name, name, err.Error()) + } + + res, err := act.WithGlobal(process.Global).WithSID(process.Sid).Exec() + if err != nil { + log.Error("[form] %s %s -> %s %s", form.ID, p.Name, name, err.Error()) + return nil, fmt.Errorf("[form] %s %s -> %s %s", form.ID, p.Name, name, err.Error()) + } + + // Compute Out + switch p.Name { + + case "yao.form.Find": + switch res.(type) { + case map[string]interface{}, maps.MapStr: + data := any.MapOf(res).MapStrAny + err := form.computeFind(process, data) + if err != nil { + log.Error("[form] %s %s -> %s %s", form.ID, p.Name, name, err.Error()) + } + res = data + } + break + } + + // After hook + if p.After != nil { + log.Trace("[form] %s %s after: exec(%v)", form.ID, p.Name, res) + newRes, err := p.After.Exec(res, process.Sid, process.Global) + if err != nil { + log.Error("[form] %s %s after: %s", form.ID, p.Name, err.Error()) + } else { + log.Trace("[form] %s %s after: %v", form.ID, p.Name, newRes) + res = newRes + } + } + + return res, nil +} diff --git a/widgets/form/api.go b/widgets/form/api.go new file mode 100644 index 00000000..db6e8a99 --- /dev/null +++ b/widgets/form/api.go @@ -0,0 +1,114 @@ +package form + +import ( + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/yao/share" +) + +// export API +func exportAPI() error { + + http := gou.HTTP{ + Name: "Widget Form API", + Description: "Widget Form API", + Version: share.VERSION, + Guard: "-", + Group: "__yao/form", + Paths: []gou.Path{}, + } + + // GET /api/__yao/form/:id/setting -> Default process: yao.form.Xgen + path := gou.Path{ + Label: "Setting", + Description: "Setting", + Path: "/:id/setting", + Method: "GET", + Process: "yao.form.Setting", + In: []string{"$param.id"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // GET /api/__yao/form/:id/find/:primary -> Default process: yao.form.Find $param.id $param.primary :query + path = gou.Path{ + Label: "Find", + Description: "Find", + Path: "/:id/find/:primary", + Method: "GET", + Process: "yao.form.Find", + In: []string{"$param.id", "$param.primary", ":query-param"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // GET /api/__yao/form/:id/component/:xpath/:method -> Default process: yao.form.Component $param.id $param.xpath $param.method :query + path = gou.Path{ + Label: "Find", + Description: "Find", + Path: "/:id/component/:xpath/:method", + Method: "GET", + Process: "yao.form.Component", + In: []string{"$param.id", "$param.xpath", "$param.method", ":query"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // POST /api/__yao/form/:id/save -> Default process: yao.form.Save $param.id :payload + path = gou.Path{ + Label: "Save", + Description: "Save", + Path: "/:id/save", + Method: "POST", + Process: "yao.form.Save", + In: []string{"$param.id", ":payload"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // POST /api/__yao/form/:id/create -> Default process: yao.form.Create $param.id :payload + path = gou.Path{ + Label: "Create", + Description: "Create", + Path: "/:id/create", + Method: "POST", + Process: "yao.form.Create", + In: []string{"$param.id", ":payload"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // POST /api/__yao/form/:id/update/:primary -> Default process: yao.form.Update $param.id $param.primary :payload + path = gou.Path{ + Label: "Update", + Description: "Update", + Path: "/:id/update/:primary", + Method: "POST", + Process: "yao.form.Update", + In: []string{"$param.id", "$param.primary", ":payload"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // POST /api/__yao/form/:id/delete/:primary -> Default process: yao.form.Delete $param.id $param.primary + path = gou.Path{ + Label: "Delete", + Description: "Delete", + Path: "/:id/delete/:primary", + Method: "POST", + Process: "yao.form.Delete", + In: []string{"$param.id", "$param.primary"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // api source + source, err := jsoniter.Marshal(http) + if err != nil { + return err + } + + // load apis + _, err = gou.LoadAPIReturn(string(source), "widgets.form") + return err +} diff --git a/widgets/form/bind.go b/widgets/form/bind.go new file mode 100644 index 00000000..bdc4f016 --- /dev/null +++ b/widgets/form/bind.go @@ -0,0 +1,53 @@ +package form + +import ( + "fmt" + + "github.com/yaoapp/gou" +) + +// Bind model / store / table / ... +func (dsl *DSL) Bind() error { + + if dsl.Action.Bind == nil { + return nil + } + + if dsl.Action.Bind.Model != "" { + return dsl.bindModel() + } + + if dsl.Action.Bind.Store != "" { + return dsl.bindStore() + } + + if dsl.Action.Bind.Table != "" { + return dsl.bindTable() + } + + return nil +} + +func (dsl *DSL) bindModel() error { + + id := dsl.Action.Bind.Model + m, has := gou.Models[id] + if !has { + return fmt.Errorf("%s does not exist", id) + } + + dsl.Action.BindModel(m) + dsl.Fields.BindModel(m) + // dsl.Layout.BindModel(m) + return nil +} + +func (dsl *DSL) bindTable() error { + id := dsl.Action.Bind.Table + return fmt.Errorf("bind.table %s does not support yet", id) +} + +func (dsl *DSL) bindStore() error { + id := dsl.Action.Bind.Store + return fmt.Errorf("bind.store %s does not support yet", id) +} diff --git a/widgets/form/compute.go b/widgets/form/compute.go new file mode 100644 index 00000000..0ce4152c --- /dev/null +++ b/widgets/form/compute.go @@ -0,0 +1,79 @@ +package form + +import ( + "fmt" + "strings" + + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/log" +) + +func (dsl *DSL) computeFind(process *gou.Process, values map[string]interface{}) error { + + messages := []string{} + for key := range values { + err := dsl.computeOut(process, key, values) + if err != nil { + messages = append(messages, err.Error()) + } + } + + if len(messages) > 0 { + return fmt.Errorf("%s", strings.Join(messages, ";")) + } + + return nil +} + +func (dsl *DSL) computeSave(process *gou.Process, values map[string]interface{}) error { + + messages := []string{} + for key := range values { + err := dsl.computeIn(process, key, values) + if err != nil { + messages = append(messages, err.Error()) + } + } + + if len(messages) > 0 { + return fmt.Errorf("%s", strings.Join(messages, ";")) + } + + return nil +} + +func (dsl *DSL) computeIn(process *gou.Process, key string, values map[string]interface{}) error { + if name, has := dsl.ComputesIn[key]; has { + compute, err := gou.ProcessOf(name, key, values[key], values) + if err != nil { + log.Error("[table] %s compute-in -> %s %s %s", dsl.ID, name, key, err.Error()) + return fmt.Errorf("[table] %s compute-in -> %s %s %s", dsl.ID, name, key, err.Error()) + } + + res, err := compute.WithGlobal(process.Global).WithSID(process.Sid).Exec() + if err != nil { + log.Error("[table] %s compute-in -> %s %s %s", dsl.ID, name, key, err.Error()) + return fmt.Errorf("[table] %s compute-in -> %s %s %s", dsl.ID, name, key, err.Error()) + } + values[key] = res + } + return nil +} + +func (dsl *DSL) computeOut(process *gou.Process, key string, values map[string]interface{}) error { + if name, has := dsl.ComputesOut[key]; has { + compute, err := gou.ProcessOf(name, key, values[key], values) + if err != nil { + log.Error("[table] %s compute-out -> %s %s %s", dsl.ID, name, key, err.Error()) + return fmt.Errorf("[table] %s compute-out -> %s %s %s", dsl.ID, name, key, err.Error()) + } + + res, err := compute.WithGlobal(process.Global).WithSID(process.Sid).Exec() + if err != nil { + log.Error("[table] %s compute-out -> %s %s %s", dsl.ID, name, key, err.Error()) + return fmt.Errorf("[table] %s compute-out -> %s %s %s", dsl.ID, name, key, err.Error()) + } + values[key] = res + } + return nil +} diff --git a/widgets/form/export.go b/widgets/form/export.go new file mode 100644 index 00000000..1edf59fd --- /dev/null +++ b/widgets/form/export.go @@ -0,0 +1,7 @@ +package form + +// Export process & api +func Export() error { + exportProcess() + return exportAPI() +} diff --git a/widgets/form/fields.go b/widgets/form/fields.go new file mode 100644 index 00000000..da223c8b --- /dev/null +++ b/widgets/form/fields.go @@ -0,0 +1,26 @@ +package form + +import ( + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" +) + +// BindModel bind model +func (fields *FieldsDSL) BindModel(m *gou.Model) { +} + +// Xgen trans to xgen setting +func (fields *FieldsDSL) Xgen() (map[string]interface{}, error) { + res := map[string]interface{}{} + data, err := jsoniter.Marshal(fields) + if err != nil { + return nil, err + } + + err = jsoniter.Unmarshal(data, &res) + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/widgets/form/form.go b/widgets/form/form.go new file mode 100644 index 00000000..91b8d71d --- /dev/null +++ b/widgets/form/form.go @@ -0,0 +1,215 @@ +package form + +import ( + "fmt" + "path/filepath" + "strings" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/gou/lang" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/widgets/component" + "github.com/yaoapp/yao/widgets/field" +) + +// +// API: +// GET /api/__yao/form/:id/setting -> Default process: yao.form.Xgen +// GET /api/__yao/form/:id/find/:primary -> Default process: yao.form.Find $param.id $param.primary :query +// GET /api/__yao/form/:id/component/:xpath/:method -> Default process: yao.form.Component $param.id $param.xpath $param.method :query +// POST /api/__yao/form/:id/save -> Default process: yao.form.Save $param.id :payload +// POST /api/__yao/form/:id/create -> Default process: yao.form.Create $param.id :payload +// POST /api/__yao/form/:id/update/:primary -> Default process: yao.form.Update $param.id $param.primary :payload +// POST /api/__yao/form/:id/delete/:primary -> Default process: yao.form.Delete $param.id $param.primary +// +// Process: +// yao.form.Setting Return the App DSL +// yao.form.Xgen Return the Xgen setting +// yao.form.Find Return the record via the given primary key +// yao.form.Component Return the result defined in props.xProps +// yao.form.Save Save a record, if given a primary key update, else insert +// yao.form.Create Create a record +// yao.form.Update update record via the given primary key +// yao.form.Delete delete record via the given primary key +// +// Hook: +// before:find +// after:find +// before:save +// after:save +// before:create +// after:create +// before:delete +// after:delete +// before:update +// after:update +// + +// Forms the loaded form widgets +var Forms map[string]*DSL = map[string]*DSL{} + +// New create a new DSL +func New(id string) *DSL { + return &DSL{ + ID: id, + Fields: &FieldsDSL{Form: field.Columns{}}, + CProps: field.CloudProps{}, + ComputesIn: field.ComputeFields{}, + ComputesOut: field.ComputeFields{}, + } +} + +// LoadAndExport load table +func LoadAndExport(cfg config.Config) error { + err := Load(cfg) + if err != nil { + return err + } + return Export() +} + +// Load load task +func Load(cfg config.Config) error { + var root = filepath.Join(cfg.Root, "forms") + return LoadFrom(root, "") +} + +// LoadFrom load from dir +func LoadFrom(dir string, prefix string) error { + + if share.DirNotExists(dir) { + return fmt.Errorf("%s does not exists", dir) + } + + messages := []string{} + err := share.Walk(dir, ".json", func(root, filename string) { + id := prefix + share.ID(root, filename) + data := share.ReadFile(filename) + dsl := New(id) + err := jsoniter.Unmarshal(data, dsl) + if err != nil { + messages = append(messages, fmt.Sprintf("[%s] %s", id, err.Error())) + return + } + + if dsl.Action == nil { + dsl.Action = &ActionDSL{} + } + dsl.Action.SetDefaultProcess() + + if dsl.Layout == nil { + dsl.Layout = &LayoutDSL{} + } + + if dsl.Fields == nil { + dsl.Fields = &FieldsDSL{} + } + + // Bind model / store / table / ... + err = dsl.Bind() + if err != nil { + messages = append(messages, fmt.Sprintf("[%s] %s", id, err.Error())) + return + } + + // Parse + err = dsl.Parse() + if err != nil { + messages = append(messages, fmt.Sprintf("[%s] %s", id, err.Error())) + return + } + + // Validate + err = dsl.Validate() + if err != nil { + messages = append(messages, fmt.Sprintf("[%s] %s", id, err.Error())) + return + } + + // Apply a language pack + if lang.Default != nil { + lang.Default.Apply(dsl) + } + + Forms[id] = dsl + }) + + if len(messages) > 0 { + return fmt.Errorf(strings.Join(messages, ";")) + } + + return err +} + +// Get form via process or id +func Get(form interface{}) (*DSL, error) { + id := "" + switch form.(type) { + case string: + id = form.(string) + case *gou.Process: + id = form.(*gou.Process).ArgsString(0) + default: + return nil, fmt.Errorf("%v type does not support", form) + } + + t, has := Forms[id] + if !has { + return nil, fmt.Errorf("%s does not exist", id) + } + return t, nil +} + +// MustGet Get form via process or id thow error +func MustGet(form interface{}) *DSL { + t, err := Get(form) + if err != nil { + exception.New(err.Error(), 400).Throw() + } + return t +} + +// Parse Layout +func (dsl *DSL) Parse() error { + + // ComputeFields + dsl.Fields.Form.ComputeFieldsMerge(dsl.ComputesIn, dsl.ComputesOut) + + // Columns + return dsl.Fields.Form.CPropsMerge(dsl.CProps, func(name string, kind string, column field.ColumnDSL) (xpath string) { + return fmt.Sprintf("fields.form.%s.%s.props", name, kind) + }) +} + +// Xgen trans to xgen setting +func (dsl *DSL) Xgen() (map[string]interface{}, error) { + + setting, err := dsl.Layout.Xgen() + if err != nil { + return nil, err + } + + fields, err := dsl.Fields.Xgen() + if err != nil { + return nil, err + } + + setting["fields"] = fields + for _, cProp := range dsl.CProps { + err := cProp.Replace(setting, func(cProp component.CloudPropsDSL) interface{} { + return map[string]interface{}{ + "api": fmt.Sprintf("/api/__yao/form/%s/component/%s/%s", dsl.ID, cProp.Xpath, cProp.Name), + "params": cProp.Query, + } + }) + + if err != nil { + return nil, err + } + } + + return setting, nil +} diff --git a/widgets/form/form_test.go b/widgets/form/form_test.go new file mode 100644 index 00000000..da8a56f3 --- /dev/null +++ b/widgets/form/form_test.go @@ -0,0 +1,55 @@ +package form + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/lang" + "github.com/yaoapp/yao/model" + "github.com/yaoapp/yao/script" + "github.com/yaoapp/yao/share" +) + +func TestLoad(t *testing.T) { + prepare(t) + err := Load(config.Conf) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 2, len(Forms)) +} + +func prepare(t *testing.T, language ...string) { + + // langs + if len(language) < 1 { + os.Unsetenv("YAO_LANG") + } else { + os.Setenv("YAO_LANG", language[0]) + } + lang.Load(config.Conf) + + share.DBConnect(config.Conf.DB) // removed later + + // load scripts + err := script.Load(config.Conf) + if err != nil { + t.Fatal(err) + } + + // load models + err = model.Load(config.Conf) + if err != nil { + t.Fatal(err) + } + + // load scripts + + // export + err = Export() + if err != nil { + t.Fatal(err) + } +} diff --git a/widgets/form/lang.go b/widgets/form/lang.go new file mode 100644 index 00000000..9b677474 --- /dev/null +++ b/widgets/form/lang.go @@ -0,0 +1,7 @@ +package form + +// Lang for applying a language pack +func (dsl *DSL) Lang(trans func(widget string, inst string, value *string) bool) { + widget := "form" + trans(widget, dsl.ID, &dsl.Name) +} diff --git a/widgets/form/layout.go b/widgets/form/layout.go new file mode 100644 index 00000000..94ad64e3 --- /dev/null +++ b/widgets/form/layout.go @@ -0,0 +1,27 @@ +package form + +import ( + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" +) + +// BindModel bind model +func (layout *LayoutDSL) BindModel(m *gou.Model) { + layout.Primary = m.PrimaryKey +} + +// Xgen trans to Xgen setting +func (layout *LayoutDSL) Xgen() (map[string]interface{}, error) { + res := map[string]interface{}{} + data, err := jsoniter.Marshal(layout) + if err != nil { + return nil, err + } + + err = jsoniter.Unmarshal(data, &res) + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/widgets/form/process.go b/widgets/form/process.go new file mode 100644 index 00000000..e279728c --- /dev/null +++ b/widgets/form/process.go @@ -0,0 +1,91 @@ +package form + +import ( + "fmt" + + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/exception" +) + +// Export process +func exportProcess() { + gou.RegisterProcessHandler("yao.form.setting", processSetting) + gou.RegisterProcessHandler("yao.form.xgen", processXgen) + gou.RegisterProcessHandler("yao.form.component", processComponent) + gou.RegisterProcessHandler("yao.form.find", processFind) + gou.RegisterProcessHandler("yao.form.save", processSave) + gou.RegisterProcessHandler("yao.form.create", processCreate) + gou.RegisterProcessHandler("yao.form.update", processUpdate) + gou.RegisterProcessHandler("yao.form.delete", processDelete) +} + +func processXgen(process *gou.Process) interface{} { + + form := MustGet(process) + setting, err := form.Xgen() + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + return setting +} + +func processComponent(process *gou.Process) interface{} { + + process.ValidateArgNums(3) + form := MustGet(process) + xpath := process.ArgsString(1) + method := process.ArgsString(2) + key := fmt.Sprintf("%s.$%s", xpath, method) + + // get cloud props + cProp, has := form.CProps[key] + if !has { + exception.New("%s does not exist", 400, key).Throw() + } + + // :query + query := map[string]interface{}{} + if process.NumOfArgsIs(4) { + query = process.ArgsMap(3) + } + + // execute query + res, err := cProp.ExecQuery(process, query) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + return res +} + +func processSetting(process *gou.Process) interface{} { + form := MustGet(process) + process.Args = append(process.Args, process.Args[0]) // formle name + return form.Action.Setting.MustExec(process) +} + +func processSave(process *gou.Process) interface{} { + form := MustGet(process) + return form.Action.Save.MustExec(process) +} + +func processCreate(process *gou.Process) interface{} { + form := MustGet(process) + return form.Action.Create.MustExec(process) +} + +func processFind(process *gou.Process) interface{} { + form := MustGet(process) + return form.Action.Find.MustExec(process) +} + +func processUpdate(process *gou.Process) interface{} { + form := MustGet(process) + return form.Action.Update.MustExec(process) +} + +func processDelete(process *gou.Process) interface{} { + form := MustGet(process) + return form.Action.Delete.MustExec(process) +} diff --git a/widgets/form/process_test.go b/widgets/form/process_test.go new file mode 100644 index 00000000..bccbfde6 --- /dev/null +++ b/widgets/form/process_test.go @@ -0,0 +1,238 @@ +package form + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/kun/maps" + "github.com/yaoapp/yao/config" + q "github.com/yaoapp/yao/query" +) + +func TestProcessFind(t *testing.T) { + + load(t) + clear(t) + testData(t) + + args := []interface{}{"pet", 1} + res, err := gou.NewProcess("yao.form.find", args...).Exec() + if err != nil { + t.Fatal(err) + } + + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "#FF0000", data.Get("status.color")) + assert.Equal(t, "status", data.Get("status.field")) + assert.Equal(t, "checked", data.Get("status.label")) + assert.Equal(t, "Cookie", data.Get("status.name")) +} + +func TestProcessSave(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{"pet", map[string]interface{}{ + "name": "New Pet", + "type": "cat", + "status": "checked", + "mode": "enabled", + "stay": 66, + "cost": 24, + "doctor_id": 1, + }} + + res, err := gou.NewProcess("yao.form.Save", args...).Exec() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "4", fmt.Sprintf("%v", res)) + + res, err = gou.NewProcess("yao.form.find", "pet", res).Exec() + if err != nil { + t.Fatal(err) + } + + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "New Pet|New Pet", data.Get("name")) +} + +func TestProcessCreate(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{"pet", map[string]interface{}{ + "id": 6, + "name": "New Pet", + "type": "cat", + "status": "checked", + "mode": "enabled", + "stay": 66, + "cost": 24, + "doctor_id": 1, + }} + + res, err := gou.NewProcess("yao.form.Create", args...).Exec() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "6", fmt.Sprintf("%v", res)) + + res, err = gou.NewProcess("yao.form.find", "pet", res).Exec() + if err != nil { + t.Fatal(err) + } + + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "New Pet|New Pet", data.Get("name")) +} + +func TestProcessUpdate(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{"pet", 1, map[string]interface{}{ + "name": "New Pet", + "type": "cat", + "status": "checked", + "mode": "enabled", + "stay": 66, + "cost": 24, + "doctor_id": 1, + }} + + _, err := gou.NewProcess("yao.form.Update", args...).Exec() + if err != nil { + t.Fatal(err) + } + + res, err := gou.NewProcess("yao.form.find", "pet", 1).Exec() + if err != nil { + t.Fatal(err) + } + + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "New Pet|New Pet", data.Get("name")) +} + +func TestProcessDelete(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{"pet", 1} + + _, err := gou.NewProcess("yao.form.Delete", args...).Exec() + if err != nil { + t.Fatal(err) + } + + _, err = gou.NewProcess("yao.form.find", "pet", 1).Exec() + assert.Contains(t, err.Error(), "ID=1") +} + +func TestProcessComponent(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{ + "pet", + "fields.form.状态.edit.props.xProps", + "remote", + map[string]interface{}{"select": []string{"name", "status"}, "limit": 2}, + } + + res, err := gou.NewProcess("yao.form.Component", args...).Exec() + if err != nil { + t.Fatal(err) + } + + pets, ok := res.([]maps.MapStr) + assert.True(t, ok) + assert.Equal(t, 2, len(pets)) + assert.Equal(t, "Cookie", pets[0]["name"]) + assert.Equal(t, "checked", pets[0]["status"]) + assert.Equal(t, "Baby", pets[1]["name"]) + assert.Equal(t, "checked", pets[1]["status"]) +} + +func TestProcessComponentError(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{ + "pet", + "fields.filter.edit.props.状态.::not-exist", + "remote", + map[string]interface{}{"select": []string{"name", "status"}, "limit": 2}, + } + _, err := gou.NewProcess("yao.form.Component", args...).Exec() + assert.Contains(t, err.Error(), "fields.filter.edit.props.状态.::not-exist") +} + +func TestProcessSetting(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{"pet"} + res, err := gou.NewProcess("yao.form.Setting", args...).Exec() + if err != nil { + t.Fatal(err) + } + + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "/api/__yao/form/pet/component/fields.form.状态.edit.props.xProps/remote", data.Get("fields.form.状态.edit.props.xProps.remote.api")) +} + +func TestProcessXgen(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{"pet"} + res, err := gou.NewProcess("yao.form.Xgen", args...).Exec() + if err != nil { + t.Fatal(err) + } + + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "/api/__yao/form/pet/component/fields.form.状态.edit.props.xProps/remote", data.Get("fields.form.状态.edit.props.xProps.remote.api")) +} + +func load(t *testing.T) { + prepare(t) + err := Load(config.Conf) + if err != nil { + t.Fatal(err) + } + q.Load(config.Conf) +} + +func testData(t *testing.T) { + pet := gou.Select("pet") + err := pet.Insert( + []string{"name", "type", "status", "mode", "stay", "cost", "doctor_id"}, + [][]interface{}{ + {"Cookie", "cat", "checked", "enabled", 200, 105, 1}, + {"Baby", "dog", "checked", "enabled", 186, 24, 1}, + {"Poo", "others", "checked", "enabled", 199, 66, 1}, + }, + ) + if err != nil { + t.Fatal(err) + } +} + +func clear(t *testing.T) { + for _, m := range gou.Models { + err := m.DropTable() + if err != nil { + t.Fatal(err) + } + err = m.Migrate(true) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/widgets/form/types.go b/widgets/form/types.go new file mode 100644 index 00000000..ed707f66 --- /dev/null +++ b/widgets/form/types.go @@ -0,0 +1,88 @@ +package form + +import ( + "github.com/yaoapp/yao/widgets/action" + "github.com/yaoapp/yao/widgets/component" + "github.com/yaoapp/yao/widgets/field" + "github.com/yaoapp/yao/widgets/hook" +) + +// DSL the form DSL +type DSL struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Action *ActionDSL `json:"action"` + Layout *LayoutDSL `json:"layout"` + Fields *FieldsDSL `json:"fields"` + ComputesIn field.ComputeFields `json:"-"` + ComputesOut field.ComputeFields `json:"-"` + CProps field.CloudProps `json:"-"` +} + +// ActionDSL the form action DSL +type ActionDSL struct { + Bind *BindActionDSL `json:"bind,omitempty"` + Setting *action.Process `json:"setting,omitempty"` + Component *action.Process `json:"component,omitempty"` + Find *action.Process `json:"find,omitempty"` + Save *action.Process `json:"save,omitempty"` + Update *action.Process `json:"update,omitempty"` + Create *action.Process `json:"create,omitempty"` + Delete *action.Process `json:"delete,omitempty"` + BeforeFind *hook.Before `json:"before:find,omitempty"` + AfterFind *hook.After `json:"after:find,omitempty"` + BeforeSave *hook.Before `json:"before:save,omitempty"` + AfterSave *hook.After `json:"after:save,omitempty"` + BeforeCreate *hook.Before `json:"before:create,omitempty"` + AfterCreate *hook.After `json:"after:create,omitempty"` + BeforeDelete *hook.Before `json:"before:delete,omitempty"` + AfterDelete *hook.After `json:"after:delete,omitempty"` + BeforeUpdate *hook.Before `json:"before:update,omitempty"` + AfterUpdate *hook.After `json:"after:update,omitempty"` +} + +// BindActionDSL action.bind +type BindActionDSL struct { + Model string `json:"model,omitempty"` // bind model + Store string `json:"store,omitempty"` // bind store + Table string `json:"table,omitempty"` // bind table + Option map[string]interface{} `json:"option,omitempty"` // bind option +} + +// LayoutDSL the form layout DSL +type LayoutDSL struct { + Primary string `json:"primary,omitempty"` + Operation *OperationLayoutDSL `json:"operation,omitempty"` + Form *ViewLayoutDSL `json:"form,omitempty"` + Config map[string]interface{} `json:"config,omitempty"` +} + +// OperationLayoutDSL layout.operation +type OperationLayoutDSL struct { + Preset map[string]map[string]interface{} `json:"preset,omitempty"` + Actions []component.ActionDSL `json:"actions,omitempty"` +} + +// FieldsDSL the form fields DSL +type FieldsDSL struct { + Form field.Columns `json:"form,omitempty"` +} + +// ViewLayoutDSL layout.form +type ViewLayoutDSL struct { + Props component.PropsDSL `json:"props,omitempty"` + Sections []SectionDSL `json:"sections,omitempty"` +} + +// SectionDSL layout.form.sections[*] +type SectionDSL struct { + Title string `json:"title,omitempty"` + Desc string `json:"desc,omitempty"` + Columns []Columns `json:"columns,omitempty"` +} + +// Columns table columns +type Columns struct { + Tabs []SectionDSL `json:"tabs,omitempty"` + component.InstanceDSL +} diff --git a/widgets/form/vaildate.go b/widgets/form/vaildate.go new file mode 100644 index 00000000..b460b42f --- /dev/null +++ b/widgets/form/vaildate.go @@ -0,0 +1,6 @@ +package form + +// Validate table +func (dsl *DSL) Validate() error { + return nil +} diff --git a/widgets/table/hook.go b/widgets/hook/hook.go similarity index 77% rename from widgets/table/hook.go rename to widgets/hook/hook.go index f078a0d1..fc9fb2d2 100644 --- a/widgets/table/hook.go +++ b/widgets/hook/hook.go @@ -1,4 +1,4 @@ -package table +package hook import ( "fmt" @@ -7,7 +7,7 @@ import ( ) // Exec execute the hook -func (hook *BeforeHookActionDSL) Exec(args []interface{}, sid string, global map[string]interface{}) ([]interface{}, error) { +func (hook *Before) Exec(args []interface{}, sid string, global map[string]interface{}) ([]interface{}, error) { p, err := gou.ProcessOf(hook.String(), args...) if err != nil { @@ -32,7 +32,7 @@ func (hook *BeforeHookActionDSL) Exec(args []interface{}, sid string, global map } // Exec execute the hook -func (hook *AfterHookActionDSL) Exec(value interface{}, sid string, global map[string]interface{}) (interface{}, error) { +func (hook *After) Exec(value interface{}, sid string, global map[string]interface{}) (interface{}, error) { args := []interface{}{} switch value.(type) { @@ -56,11 +56,11 @@ func (hook *AfterHookActionDSL) Exec(value interface{}, sid string, global map[s } // String cast to string -func (hook *BeforeHookActionDSL) String() string { +func (hook *Before) String() string { return string(*hook) } // String cast to string -func (hook *AfterHookActionDSL) String() string { +func (hook *After) String() string { return string(*hook) } diff --git a/widgets/hook/types.go b/widgets/hook/types.go new file mode 100644 index 00000000..d119844a --- /dev/null +++ b/widgets/hook/types.go @@ -0,0 +1,7 @@ +package hook + +// Before before:search ... +type Before string + +// After after:search ... +type After string diff --git a/widgets/login/login.go b/widgets/login/login.go index 64ac2e70..78d9284a 100644 --- a/widgets/login/login.go +++ b/widgets/login/login.go @@ -21,7 +21,16 @@ import ( // Logins the loaded login widgets var Logins map[string]*DSL = map[string]*DSL{} -// Load load task +// LoadAndExport load login +func LoadAndExport(cfg config.Config) error { + err := Load(cfg) + if err != nil { + return err + } + return Export() +} + +// Load load login func Load(cfg config.Config) error { var root = filepath.Join(cfg.Root, "logins") return LoadFrom(root, "") diff --git a/widgets/table/action.go b/widgets/table/action.go index be7dcb4b..995ca1c6 100644 --- a/widgets/table/action.go +++ b/widgets/table/action.go @@ -5,12 +5,12 @@ import ( "github.com/yaoapp/gou" "github.com/yaoapp/kun/any" - "github.com/yaoapp/kun/exception" "github.com/yaoapp/kun/log" "github.com/yaoapp/kun/maps" + "github.com/yaoapp/yao/widgets/action" ) -var processActionDefaults = map[string]*ProcessActionDSL{ +var processActionDefaults = map[string]*action.Process{ "Setting": { Name: "yao.table.Setting", @@ -72,113 +72,104 @@ var processActionDefaults = map[string]*ProcessActionDSL{ } // SetDefaultProcess set the default value of action -func (action *ActionDSL) SetDefaultProcess() { - action.Setting = action.newProcessAction("Setting", action.Setting, nil, nil) - action.Component = action.newProcessAction("Component", action.Component, nil, nil) - action.Search = action.newProcessAction("Search", action.Search, action.BeforeSearch, action.AfterSearch) - action.Get = action.newProcessAction("Get", action.Get, action.BeforeGet, action.AfterGet) - action.Find = action.newProcessAction("Find", action.Find, action.BeforeFind, action.AfterFind) - action.Save = action.newProcessAction("Save", action.Save, action.BeforeSave, action.AfterSave) - action.Create = action.newProcessAction("Create", action.Create, action.BeforeCreate, action.AfterCreate) - action.Insert = action.newProcessAction("Insert", action.Insert, action.BeforeInsert, action.AfterInsert) - action.Update = action.newProcessAction("Update", action.Update, action.BeforeUpdate, action.AfterUpdate) - action.UpdateWhere = action.newProcessAction("UpdateWhere", action.UpdateWhere, action.BeforeUpdateWhere, action.AfterUpdateWhere) - action.UpdateIn = action.newProcessAction("UpdateIn", action.UpdateIn, action.BeforeUpdateIn, action.AfterUpdateIn) - action.Delete = action.newProcessAction("Delete", action.Delete, action.BeforeDelete, action.AfterDelete) - action.DeleteWhere = action.newProcessAction("DeleteWhere", action.DeleteWhere, action.BeforeDeleteWhere, action.AfterDeleteWhere) - action.DeleteIn = action.newProcessAction("DeleteIn", action.DeleteIn, action.BeforeDeleteIn, action.AfterDeleteIn) +func (act *ActionDSL) SetDefaultProcess() { + + act.Setting = action.NewProcess("Setting", act.Setting). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Component = action.NewProcess("Component", act.Component). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Search = action.NewProcess("Search", act.Search). + WithBefore(act.BeforeSearch).WithAfter(act.AfterSearch). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Get = action.NewProcess("Get", act.Get). + WithBefore(act.BeforeGet).WithAfter(act.AfterGet). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Find = action.NewProcess("Find", act.Find). + WithBefore(act.BeforeFind). + WithAfter(act.AfterFind). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Save = action.NewProcess("Save", act.Save). + WithBefore(act.BeforeSave).WithAfter(act.AfterSave). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Create = action.NewProcess("Create", act.Create). + WithBefore(act.BeforeCreate).WithAfter(act.AfterCreate). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Insert = action.NewProcess("Insert", act.Insert). + WithBefore(act.BeforeInsert).WithAfter(act.AfterInsert). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Update = action.NewProcess("Update", act.Update). + WithBefore(act.BeforeUpdate).WithAfter(act.AfterUpdate). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.UpdateWhere = action.NewProcess("UpdateWhere", act.UpdateWhere). + WithBefore(act.BeforeUpdateWhere).WithAfter(act.AfterUpdateWhere). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.UpdateIn = action.NewProcess("UpdateIn", act.UpdateIn). + WithBefore(act.BeforeUpdateIn).WithAfter(act.AfterUpdateIn). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.Delete = action.NewProcess("Delete", act.Delete). + WithBefore(act.BeforeDelete).WithAfter(act.AfterDelete). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.DeleteWhere = action.NewProcess("DeleteWhere", act.DeleteWhere). + WithBefore(act.BeforeDeleteWhere).WithAfter(act.AfterDeleteWhere). + SetDefault(processActionDefaults). + SetHandler(processHandler) + + act.DeleteIn = action.NewProcess("DeleteIn", act.DeleteIn). + WithBefore(act.BeforeDeleteIn).WithAfter(act.AfterDeleteIn). + SetDefault(processActionDefaults). + SetHandler(processHandler) } // BindModel bind model -func (action *ActionDSL) BindModel(m *gou.Model) { - name := m.ID // should be id - action.Search.Bind(fmt.Sprintf("models.%s.Paginate", name)) - action.Get.Bind(fmt.Sprintf("models.%s.Get", name)) - action.Find.Bind(fmt.Sprintf("models.%s.Find", name)) - action.Save.Bind(fmt.Sprintf("models.%s.Save", name)) - action.Create.Bind(fmt.Sprintf("models.%s.Create", name)) - action.Insert.Bind(fmt.Sprintf("models.%s.Insert", name)) - action.Update.Bind(fmt.Sprintf("models.%s.Update", name)) - action.UpdateWhere.Bind(fmt.Sprintf("models.%s.UpdateWhere", name)) - action.UpdateIn.Bind(fmt.Sprintf("models.%s.UpdateWhere", name)) - action.Delete.Bind(fmt.Sprintf("models.%s.Delete", name)) - action.DeleteWhere.Bind(fmt.Sprintf("models.%s.DeleteWhere", name)) - action.DeleteIn.Bind(fmt.Sprintf("models.%s.DeleteWhere", name)) +func (act *ActionDSL) BindModel(m *gou.Model) { + + name := m.ID + act.Search.Bind(fmt.Sprintf("models.%s.Paginate", name)) + act.Get.Bind(fmt.Sprintf("models.%s.Get", name)) + act.Find.Bind(fmt.Sprintf("models.%s.Find", name)) + act.Save.Bind(fmt.Sprintf("models.%s.Save", name)) + act.Create.Bind(fmt.Sprintf("models.%s.Create", name)) + act.Insert.Bind(fmt.Sprintf("models.%s.Insert", name)) + act.Update.Bind(fmt.Sprintf("models.%s.Update", name)) + act.UpdateWhere.Bind(fmt.Sprintf("models.%s.UpdateWhere", name)) + act.UpdateIn.Bind(fmt.Sprintf("models.%s.UpdateWhere", name)) + act.Delete.Bind(fmt.Sprintf("models.%s.Delete", name)) + act.DeleteWhere.Bind(fmt.Sprintf("models.%s.DeleteWhere", name)) + act.DeleteIn.Bind(fmt.Sprintf("models.%s.DeleteWhere", name)) // bind options - if action.Bind.Option != nil { - action.Search.Default[0] = action.Bind.Option - action.Get.Default[0] = action.Bind.Option - action.Find.Default[1] = action.Bind.Option + if act.Bind.Option != nil { + act.Search.Default[0] = act.Bind.Option + act.Get.Default[0] = act.Bind.Option + act.Find.Default[1] = act.Bind.Option } } -// setDefault Set the process action disabled -func (action *ActionDSL) newProcessAction(name string, p *ProcessActionDSL, before *BeforeHookActionDSL, after *AfterHookActionDSL) *ProcessActionDSL { - - if p == nil { - p = &ProcessActionDSL{} - } - - p.After = after - p.Before = before - - if defaultProcess, has := processActionDefaults[name]; has { - - p.Name = defaultProcess.Name - - if p.Process == "" { - p.Process = defaultProcess.Process - } - - if p.Guard == "" { - p.Guard = defaultProcess.Guard - } - - if p.Default == nil { - p.Default = defaultProcess.Default - } - - // format defaults - if len(p.Default) != len(defaultProcess.Default) { - defauts := defaultProcess.Default - nums := len(p.Default) - if nums > len(defaultProcess.Default) { - nums = len(defaultProcess.Default) - } - for i := 0; i < nums; i++ { - defauts[i] = p.Default[i] - } - p.Default = defauts - } - - } - - return p -} - -// Bind the process name -func (p *ProcessActionDSL) Bind(name string) { - p.ProcessBind = name -} - -// Args get the process args -func (p *ProcessActionDSL) Args(process *gou.Process) []interface{} { - process.ValidateArgNums(1) - args := p.Default - nums := len(process.Args[1:]) - if nums > len(args) { - nums = len(args) - } - - for i := 0; i < nums; i++ { - args[i] = p.deepMergeDefault(process.Args[i+1], args[i]) - } - return args -} - -// Exec exec the process -func (p *ProcessActionDSL) Exec(process *gou.Process) (interface{}, error) { +func processHandler(p *action.Process, process *gou.Process) (interface{}, error) { tab, err := Get(process) if err != nil { @@ -222,6 +213,7 @@ func (p *ProcessActionDSL) Exec(process *gou.Process) (interface{}, error) { args[0] = data } break + case "yao.table.Update", "yao.table.UpdateWhere", "yao.table.UpdateIn": switch args[1].(type) { case map[string]interface{}: @@ -233,6 +225,7 @@ func (p *ProcessActionDSL) Exec(process *gou.Process) (interface{}, error) { args[1] = data } break + case "yao.table.Insert": break } @@ -264,7 +257,6 @@ func (p *ProcessActionDSL) Exec(process *gou.Process) (interface{}, error) { break case "yao.table.Get": - if _, ok := res.([]maps.MapStr); ok { data := []interface{}{} for _, v := range res.([]maps.MapStr) { @@ -310,50 +302,3 @@ func (p *ProcessActionDSL) Exec(process *gou.Process) (interface{}, error) { return res, nil } - -// MustExec exec the process -func (p *ProcessActionDSL) MustExec(process *gou.Process) interface{} { - res, err := p.Exec(process) - if err != nil { - exception.New(err.Error(), 500).Throw() - } - return res -} - -// deepMergeDefault deep merge args -func (p *ProcessActionDSL) deepMergeDefault(value interface{}, defaults interface{}) interface{} { - - if value == nil { - return defaults - } - - switch defaults.(type) { - - case map[string]interface{}: - defaultMap := defaults.(map[string]interface{}) - valueMap := any.Of(value).MapStr() - for key, v := range defaultMap { - valueMap[key] = p.deepMergeDefault(valueMap[key], v) - } - return valueMap - - case []interface{}: - defaultArr := defaults.([]interface{}) - valueArr := any.Of(value).CArray() - - // pad - nums := len(defaultArr) - len(valueArr) - for i := 0; i < nums; i++ { - valueArr = append(valueArr, nil) - } - - // set default - for idx, v := range defaultArr { - valueArr[idx] = p.deepMergeDefault(valueArr[idx], v) - } - - return valueArr - } - - return value -} diff --git a/widgets/table/table.go b/widgets/table/table.go index 9308c271..bf9370e4 100644 --- a/widgets/table/table.go +++ b/widgets/table/table.go @@ -12,6 +12,7 @@ import ( "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/share" "github.com/yaoapp/yao/widgets/component" + "github.com/yaoapp/yao/widgets/field" ) // @@ -80,13 +81,23 @@ var Tables map[string]*DSL = map[string]*DSL{} func New(id string) *DSL { return &DSL{ ID: id, - CProps: map[string]component.CloudPropsDSL{}, - ComputesIn: map[string]string{}, - ComputesOut: map[string]string{}, + Fields: &FieldsDSL{Filter: field.Filters{}, Table: field.Columns{}}, + CProps: field.CloudProps{}, + ComputesIn: field.ComputeFields{}, + ComputesOut: field.ComputeFields{}, } } -// Load load task +// LoadAndExport load table +func LoadAndExport(cfg config.Config) error { + err := Load(cfg) + if err != nil { + return err + } + return Export() +} + +// Load load table func Load(cfg config.Config) error { var root = filepath.Join(cfg.Root, "tables") return LoadFrom(root, "") @@ -190,14 +201,22 @@ func MustGet(table interface{}) *DSL { // Parse Layout func (dsl *DSL) Parse() error { - // init - if dsl.Fields == nil { - dsl.Fields = &FieldsDSL{ - Filter: map[string]FilterFiledsDSL{}, - Table: map[string]ViewFiledsDSL{}, - } + // ComputeFields + dsl.Fields.Table.ComputeFieldsMerge(dsl.ComputesIn, dsl.ComputesOut) + + // Filters + err := dsl.Fields.Filter.CPropsMerge(dsl.CProps, func(name string, filter field.FilterDSL) (xpath string) { + return fmt.Sprintf("fields.filter.%s.edit.props", name) + }) + + if err != nil { + return err } - return dsl.parseProps() + + // Columns + return dsl.Fields.Table.CPropsMerge(dsl.CProps, func(name string, kind string, column field.ColumnDSL) (xpath string) { + return fmt.Sprintf("fields.table.%s.%s.props", name, kind) + }) } // Xgen trans to xgen setting @@ -229,61 +248,3 @@ func (dsl *DSL) Xgen() (map[string]interface{}, error) { return setting, nil } - -// parseCloudProps parse the props -func (dsl *DSL) parseProps() error { - - // filter - for name, filter := range dsl.Fields.Filter { - if filter.Edit != nil && filter.Edit.Props != nil { - xpath := fmt.Sprintf("fields.filter.%s.edit.props", name) - cProps, err := filter.Edit.Props.CloudProps(xpath) - if err != nil { - return err - } - dsl.mergeCProps(cProps) - } - } - - // table - for name, column := range dsl.Fields.Table { - - // Computes - if column.In != "" { - dsl.ComputesIn[column.Bind] = column.In - dsl.ComputesIn[name] = column.In - } - - if column.Out != "" { - dsl.ComputesOut[column.Bind] = column.Out - dsl.ComputesOut[name] = column.Out - } - - // Cloud Props - if column.View != nil && column.View.Props != nil { - xpath := fmt.Sprintf("fields.table.%s.view.props", name) - cProps, err := column.View.Props.CloudProps(xpath) - if err != nil { - return err - } - dsl.mergeCProps(cProps) - } - - if column.Edit != nil && column.Edit.Props != nil { - xpath := fmt.Sprintf("fields.table.%s.edit.props", name) - cProps, err := column.Edit.Props.CloudProps(xpath) - if err != nil { - return err - } - dsl.mergeCProps(cProps) - } - } - - return nil -} - -func (dsl *DSL) mergeCProps(cProps map[string]component.CloudPropsDSL) { - for k, v := range cProps { - dsl.CProps[k] = v - } -} diff --git a/widgets/table/types.go b/widgets/table/types.go index c525ee32..91d4a9d7 100644 --- a/widgets/table/types.go +++ b/widgets/table/types.go @@ -1,60 +1,65 @@ package table -import "github.com/yaoapp/yao/widgets/component" +import ( + "github.com/yaoapp/yao/widgets/action" + "github.com/yaoapp/yao/widgets/component" + "github.com/yaoapp/yao/widgets/field" + "github.com/yaoapp/yao/widgets/hook" +) // DSL the table DSL type DSL struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Action *ActionDSL `json:"action"` - Layout *LayoutDSL `json:"layout"` - Fields *FieldsDSL `json:"fields"` - ComputesIn map[string]string `json:"-"` - ComputesOut map[string]string `json:"-"` - CProps map[string]component.CloudPropsDSL `json:"-"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Action *ActionDSL `json:"action"` + Layout *LayoutDSL `json:"layout"` + Fields *FieldsDSL `json:"fields"` + ComputesIn field.ComputeFields `json:"-"` + ComputesOut field.ComputeFields `json:"-"` + CProps field.CloudProps `json:"-"` } // ActionDSL the table action DSL type ActionDSL struct { - Bind *BindActionDSL `json:"bind,omitempty"` - Setting *ProcessActionDSL `json:"setting,omitempty"` - Component *ProcessActionDSL `json:"component,omitempty"` - Search *ProcessActionDSL `json:"search,omitempty"` - Get *ProcessActionDSL `json:"get,omitempty"` - Find *ProcessActionDSL `json:"find,omitempty"` - Save *ProcessActionDSL `json:"save,omitempty"` - Create *ProcessActionDSL `json:"create,omitempty"` - Insert *ProcessActionDSL `json:"insert,omitempty"` - Delete *ProcessActionDSL `json:"delete,omitempty"` - DeleteIn *ProcessActionDSL `json:"delete-in,omitempty"` - DeleteWhere *ProcessActionDSL `json:"delete-where,omitempty"` - Update *ProcessActionDSL `json:"update,omitempty"` - UpdateIn *ProcessActionDSL `json:"update-in,omitempty"` - UpdateWhere *ProcessActionDSL `json:"update-where,omitempty"` - BeforeFind *BeforeHookActionDSL `json:"before:find,omitempty"` - AfterFind *AfterHookActionDSL `json:"after:find,omitempty"` - BeforeSearch *BeforeHookActionDSL `json:"before:search,omitempty"` - AfterSearch *AfterHookActionDSL `json:"after:search,omitempty"` - BeforeGet *BeforeHookActionDSL `json:"before:get,omitempty"` - AfterGet *AfterHookActionDSL `json:"after:get,omitempty"` - BeforeSave *BeforeHookActionDSL `json:"before:save,omitempty"` - AfterSave *AfterHookActionDSL `json:"after:save,omitempty"` - BeforeCreate *BeforeHookActionDSL `json:"before:create,omitempty"` - AfterCreate *AfterHookActionDSL `json:"after:create,omitempty"` - BeforeInsert *BeforeHookActionDSL `json:"before:insert,omitempty"` - AfterInsert *AfterHookActionDSL `json:"after:insert,omitempty"` - BeforeDelete *BeforeHookActionDSL `json:"before:delete,omitempty"` - AfterDelete *AfterHookActionDSL `json:"after:delete,omitempty"` - BeforeDeleteIn *BeforeHookActionDSL `json:"before:delete-in,omitempty"` - AfterDeleteIn *AfterHookActionDSL `json:"after:delete-in,omitempty"` - BeforeDeleteWhere *BeforeHookActionDSL `json:"before:delete-where,omitempty"` - AfterDeleteWhere *AfterHookActionDSL `json:"after:delete-where,omitempty"` - BeforeUpdate *BeforeHookActionDSL `json:"before:update,omitempty"` - AfterUpdate *AfterHookActionDSL `json:"after:update,omitempty"` - BeforeUpdateIn *BeforeHookActionDSL `json:"before:update-in,omitempty"` - AfterUpdateIn *AfterHookActionDSL `json:"after:update-in,omitempty"` - BeforeUpdateWhere *BeforeHookActionDSL `json:"before:update-where,omitempty"` - AfterUpdateWhere *AfterHookActionDSL `json:"after:update-where,omitempty"` + Bind *BindActionDSL `json:"bind,omitempty"` + Setting *action.Process `json:"setting,omitempty"` + Component *action.Process `json:"component,omitempty"` + Search *action.Process `json:"search,omitempty"` + Get *action.Process `json:"get,omitempty"` + Find *action.Process `json:"find,omitempty"` + Save *action.Process `json:"save,omitempty"` + Create *action.Process `json:"create,omitempty"` + Insert *action.Process `json:"insert,omitempty"` + Delete *action.Process `json:"delete,omitempty"` + DeleteIn *action.Process `json:"delete-in,omitempty"` + DeleteWhere *action.Process `json:"delete-where,omitempty"` + Update *action.Process `json:"update,omitempty"` + UpdateIn *action.Process `json:"update-in,omitempty"` + UpdateWhere *action.Process `json:"update-where,omitempty"` + BeforeFind *hook.Before `json:"before:find,omitempty"` + AfterFind *hook.After `json:"after:find,omitempty"` + BeforeSearch *hook.Before `json:"before:search,omitempty"` + AfterSearch *hook.After `json:"after:search,omitempty"` + BeforeGet *hook.Before `json:"before:get,omitempty"` + AfterGet *hook.After `json:"after:get,omitempty"` + BeforeSave *hook.Before `json:"before:save,omitempty"` + AfterSave *hook.After `json:"after:save,omitempty"` + BeforeCreate *hook.Before `json:"before:create,omitempty"` + AfterCreate *hook.After `json:"after:create,omitempty"` + BeforeInsert *hook.Before `json:"before:insert,omitempty"` + AfterInsert *hook.After `json:"after:insert,omitempty"` + BeforeDelete *hook.Before `json:"before:delete,omitempty"` + AfterDelete *hook.After `json:"after:delete,omitempty"` + BeforeDeleteIn *hook.Before `json:"before:delete-in,omitempty"` + AfterDeleteIn *hook.After `json:"after:delete-in,omitempty"` + BeforeDeleteWhere *hook.Before `json:"before:delete-where,omitempty"` + AfterDeleteWhere *hook.After `json:"after:delete-where,omitempty"` + BeforeUpdate *hook.Before `json:"before:update,omitempty"` + AfterUpdate *hook.After `json:"after:update,omitempty"` + BeforeUpdateIn *hook.Before `json:"before:update-in,omitempty"` + AfterUpdateIn *hook.After `json:"after:update-in,omitempty"` + BeforeUpdateWhere *hook.Before `json:"before:update-where,omitempty"` + AfterUpdateWhere *hook.After `json:"after:update-where,omitempty"` } // BindActionDSL action.bind @@ -65,24 +70,6 @@ type BindActionDSL struct { Option map[string]interface{} `json:"option,omitempty"` // bind option } -// BeforeHookActionDSL action.before:search ... -type BeforeHookActionDSL string - -// AfterHookActionDSL action.after:search ... -type AfterHookActionDSL string - -// ProcessActionDSL action.search ... -type ProcessActionDSL struct { - Name string `json:"-"` - Process string `json:"process,omitempty"` - ProcessBind string `json:"bind,omitempty"` - Guard string `json:"guard,omitempty"` - Default []interface{} `json:"default,omitempty"` - Disable bool `json:"disable,omitempty"` - Before *BeforeHookActionDSL `json:"-"` - After *AfterHookActionDSL `json:"-"` -} - // LayoutDSL the table layout type LayoutDSL struct { Primary string `json:"primary,omitempty"` @@ -123,40 +110,25 @@ type OperationImportDSL struct { // FilterLayoutDSL layout.filter type FilterLayoutDSL struct { - BtnAddText string `json:"btnAddText,omitempty"` - Columns []component.InstanceDSL `json:"columns,omitempty"` + BtnAddText string `json:"btnAddText,omitempty"` + Columns component.Instances `json:"columns,omitempty"` } // ViewLayoutDSL layout.table type ViewLayoutDSL struct { - Props component.PropsDSL `json:"props,omitempty"` - Columns []component.InstanceDSL `json:"columns,omitempty"` - Operation OperationTableDSL `json:"operation,omitempty"` + Props component.PropsDSL `json:"props,omitempty"` + Columns component.Instances `json:"columns,omitempty"` + Operation OperationTableDSL `json:"operation,omitempty"` } // OperationTableDSL layout.table.operation type OperationTableDSL struct { - Fold bool `json:"fold,omitempty"` - Actions []component.ActionDSL `json:"actions,omitempty"` + Fold bool `json:"fold,omitempty"` + Actions component.Actions `json:"actions,omitempty"` } // FieldsDSL the table fields DSL type FieldsDSL struct { - Filter map[string]FilterFiledsDSL `json:"filter,omitempty"` - Table map[string]ViewFiledsDSL `json:"table,omitempty"` -} - -// FilterFiledsDSL fields.filter -type FilterFiledsDSL struct { - Bind string `json:"bind,omitempty"` - Edit *component.DSL `json:"edit,omitempty"` -} - -// ViewFiledsDSL fields.table -type ViewFiledsDSL struct { - Bind string `json:"bind,omitempty"` - In string `json:"in,omitempty"` - Out string `json:"out,omitempty"` - View *component.DSL `json:"view,omitempty"` - Edit *component.DSL `json:"edit,omitempty"` + Filter field.Filters `json:"filter,omitempty"` + Table field.Columns `json:"table,omitempty"` } diff --git a/widgets/widgets.go b/widgets/widgets.go index 35698dca..e780c612 100644 --- a/widgets/widgets.go +++ b/widgets/widgets.go @@ -3,6 +3,7 @@ package widgets import ( "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/widgets/app" + "github.com/yaoapp/yao/widgets/form" "github.com/yaoapp/yao/widgets/login" "github.com/yaoapp/yao/widgets/table" ) @@ -11,32 +12,25 @@ import ( func Load(cfg config.Config) error { // login widget - err := login.Load(cfg) - if err != nil { - return err - } - - err = login.Export() + err := login.LoadAndExport(cfg) if err != nil { return err } // app widget - err = app.Load(cfg) - if err != nil { - return err - } - err = app.Export() + err = app.LoadAndExport(cfg) if err != nil { return err } // table widget - err = table.Load(cfg) + err = table.LoadAndExport(cfg) if err != nil { return err } - err = table.Export() + + // form widget + err = form.LoadAndExport(cfg) if err != nil { return err }