diff --git a/widgets/component/component.go b/widgets/component/component.go new file mode 100644 index 00000000..a7261b97 --- /dev/null +++ b/widgets/component/component.go @@ -0,0 +1,136 @@ +package component + +import ( + "fmt" + "strings" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/log" +) + +// CloudProps parse CloudProps +func (p PropsDSL) CloudProps(xpath string) (map[string]CloudPropsDSL, error) { + return p.parseCloudProps(xpath, p) +} + +// ExecQuery execute query +func (cProp CloudPropsDSL) ExecQuery(process *gou.Process, query map[string]interface{}) (interface{}, error) { + + if query == nil { + query = map[string]interface{}{} + } + + // Process + name := cProp.Process + if name == "" { + log.Error("[component] %s.$%s process is required", cProp.Xpath, cProp.Name) + return nil, fmt.Errorf("[component] %s.$%s process is required", cProp.Xpath, cProp.Name) + } + + // Create process + p, err := gou.ProcessOf(name, query) + if err != nil { + log.Error("[component] %s.$%s %s", cProp.Xpath, cProp.Name, err.Error()) + return nil, fmt.Errorf("[component] %s.$%s %s", cProp.Xpath, cProp.Name, err.Error()) + } + + // Excute process + res, err := p.WithGlobal(process.Global).WithSID(process.Sid).Exec() + if err != nil { + log.Error("[component] %s.$%s %s", cProp.Xpath, cProp.Name, err.Error()) + return nil, fmt.Errorf("[component] %s.$%s %s", cProp.Xpath, cProp.Name, err.Error()) + } + + return res, nil +} + +// Replace xpath +func (cProp CloudPropsDSL) Replace(data interface{}, replace func(cProp CloudPropsDSL) interface{}) error { + return cProp.replaceAny(data, "", replace) +} + +func (cProp CloudPropsDSL) replaceAny(data interface{}, root string, replace func(cProp CloudPropsDSL) interface{}) error { + switch data.(type) { + case map[string]interface{}: + return cProp.replaceMap(data.(map[string]interface{}), root, replace) + // case []interface{}: + } + return nil +} + +func (cProp CloudPropsDSL) replaceMap(data map[string]interface{}, root string, replace func(cProp CloudPropsDSL) interface{}) error { + xpath := fmt.Sprintf(".%s.$%s", cProp.Xpath, cProp.Name) + for key := range data { + path := fmt.Sprintf("%s.%s", root, key) + if !strings.HasPrefix(xpath, path) { + continue + } + + // Replace field + if path == xpath { + data[cProp.Name] = replace(cProp) + delete(data, fmt.Sprintf("$%s", cProp.Name)) + continue + } + + err := cProp.replaceAny(data[key], path, replace) + if err != nil { + return err + } + } + return nil +} + +func (p PropsDSL) parseCloudProps(xpath string, props map[string]interface{}) (map[string]CloudPropsDSL, error) { + + res := map[string]CloudPropsDSL{} + + for name, prop := range props { + + fullname := fmt.Sprintf("%s.%s", xpath, name) + if sub, ok := prop.(map[string]interface{}); ok { + cProps, err := p.parseCloudProps(fullname, sub) + if err != nil { + return nil, err + } + for k, v := range cProps { + res[k] = v + } + } + + if !strings.HasPrefix(name, "$") { + continue + } + + cProp := &CloudPropsDSL{ + Name: strings.TrimPrefix(name, "$"), + Xpath: xpath, + } + + err := cProp.Parse(prop) + if err != nil { + return nil, fmt.Errorf("%s %s", fullname, err.Error()) + } + + cProp.Xpath = xpath + res[fullname] = *cProp + } + + return res, nil +} + +// Parse parse cloud props +func (cProp *CloudPropsDSL) Parse(v interface{}) error { + + bytes, err := jsoniter.Marshal(v) + if err != nil { + return err + } + + err = jsoniter.Unmarshal(bytes, cProp) + if err != nil { + return err + } + return nil +} diff --git a/widgets/component/types.go b/widgets/component/types.go index 5e526066..63ca7193 100644 --- a/widgets/component/types.go +++ b/widgets/component/types.go @@ -36,3 +36,11 @@ type PropsDSL map[string]interface{} // ParamsDSL action params type ParamsDSL map[string]interface{} + +// CloudPropsDSL the cloud props +type CloudPropsDSL struct { + Xpath string `json:"xpath,omitempty"` + Name string `json:"name,omitempty"` + Process string `json:"process,omitempty"` + Query map[string]interface{} `json:"query,omitempty"` +} diff --git a/widgets/table/fields.go b/widgets/table/fields.go index 792d0f00..38230a28 100644 --- a/widgets/table/fields.go +++ b/widgets/table/fields.go @@ -1,7 +1,26 @@ package table -import "github.com/yaoapp/gou" +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/table/layout.go b/widgets/table/layout.go index 775a22ad..c5f52b71 100644 --- a/widgets/table/layout.go +++ b/widgets/table/layout.go @@ -1,6 +1,9 @@ package table import ( + "fmt" + + jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou" ) @@ -8,3 +11,36 @@ import ( 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 + } + + // replace import + if layout.Header != nil && layout.Header.Preset != nil && layout.Header.Preset.Import != nil { + name := layout.Header.Preset.Import.Name + operation := layout.Header.Preset.Import.Operation + res["header"].(map[string]interface{})["preset"].(map[string]interface{})["import"] = map[string]interface{}{ + "api": map[string]interface{}{ + "setting": fmt.Sprintf("/api/xiang/import/%s/setting", name), + "mapping": fmt.Sprintf("/api/xiang/import/%s/mapping", name), + "preview": fmt.Sprintf("/api/xiang/import/%s/data", name), + "import": fmt.Sprintf("/api/xiang/import/%s", name), + "mapping_setting_model": fmt.Sprintf("import_%s_mapping", name), + "preview_setting_model": fmt.Sprintf("import_%s_preview", name), + }, + "operation": operation, + } + } + + return res, nil +} diff --git a/widgets/table/process.go b/widgets/table/process.go index e132ad1e..eb9068a9 100644 --- a/widgets/table/process.go +++ b/widgets/table/process.go @@ -1,9 +1,11 @@ package table import ( + "fmt" "strings" "github.com/yaoapp/gou" + "github.com/yaoapp/kun/exception" ) // Export process @@ -27,11 +29,43 @@ func exportProcess() { } func processXgen(process *gou.Process) interface{} { - return nil + + tab := MustGet(process) + setting, err := tab.Xgen() + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + return setting } func processComponent(process *gou.Process) interface{} { - return nil + + process.ValidateArgNums(3) + tab := MustGet(process) + xpath := process.ArgsString(1) + method := process.ArgsString(2) + key := fmt.Sprintf("%s.$%s", xpath, method) + + // get cloud props + cProp, has := tab.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{} { diff --git a/widgets/table/process_test.go b/widgets/table/process_test.go index 7b6e0b52..36ef126c 100644 --- a/widgets/table/process_test.go +++ b/widgets/table/process_test.go @@ -7,6 +7,7 @@ import ( "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" ) @@ -308,6 +309,62 @@ func TestProcessDeleteIn(t *testing.T) { assert.Contains(t, err.Error(), "ID=1") } +func TestProcessComponent(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{ + "pet", + "fields.filter.状态.edit.props.xProps", + "remote", + map[string]interface{}{"select": []string{"name", "status"}, "limit": 2}, + } + + res, err := gou.NewProcess("yao.table.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.table.Component", args...).Exec() + assert.Contains(t, err.Error(), "fields.filter.edit.props.状态.::not-exist") +} + +func TestProcessXgen(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{"pet"} + res, err := gou.NewProcess("yao.table.Xgen", args...).Exec() + if err != nil { + t.Fatal(err) + } + + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "/api/xiang/import/pet", data.Get("header.preset.import.api.import")) + assert.Equal(t, "跳转", data.Get("header.preset.import.operation.0.title")) + assert.Equal(t, "/api/__yao/table/pet/component/fields.table.入院状态.view.props.xProps/remote", data.Get("fields.table.入院状态.view.props.xProps.remote.api")) + assert.Equal(t, "/api/__yao/table/pet/component/fields.table.入院状态.edit.props.xProps/remote", data.Get("fields.table.入院状态.edit.props.xProps.remote.api")) +} + func load(t *testing.T) { prepare(t) err := Load(config.Conf) diff --git a/widgets/table/table.go b/widgets/table/table.go index 283932d8..9308c271 100644 --- a/widgets/table/table.go +++ b/widgets/table/table.go @@ -20,7 +20,7 @@ import ( // GET /api/__yao/table/:id/search -> Default process: yao.table.Search $param.id :query $query.page $query.pagesize // GET /api/__yao/table/:id/get -> Default process: yao.table.Get $param.id :query // GET /api/__yao/table/:id/find/:primary -> Default process: yao.table.Find $param.id $param.primary :query -// GET /api/__yao/table/:id/component/:name/:method -> Default process: yao.table.Component $param.id $param.name $param.method :query +// GET /api/__yao/table/:id/component/:xpath/:method -> Default process: yao.table.Component $param.id $param.xpath $param.method :query // POST /api/__yao/table/:id/save -> Default process: yao.table.Save $param.id :payload // POST /api/__yao/table/:id/create -> Default process: yao.table.Create $param.id :payload // POST /api/__yao/table/:id/insert -> Default process: yao.table.Insert :payload @@ -80,7 +80,7 @@ var Tables map[string]*DSL = map[string]*DSL{} func New(id string) *DSL { return &DSL{ ID: id, - Components: map[string]*component.DSL{}, + CProps: map[string]component.CloudPropsDSL{}, ComputesIn: map[string]string{}, ComputesOut: map[string]string{}, } @@ -131,7 +131,11 @@ func LoadFrom(dir string, prefix string) error { } // Parse - dsl.Parse() + err = dsl.Parse() + if err != nil { + messages = append(messages, fmt.Sprintf("[%s] %s", id, err.Error())) + return + } // Validate err = dsl.Validate() @@ -178,32 +182,108 @@ func Get(table interface{}) (*DSL, error) { func MustGet(table interface{}) *DSL { t, err := Get(table) if err != nil { - exception.New(err.Error(), 500).Throw() + exception.New(err.Error(), 400).Throw() } return t } -// Parse Layout default -func (dsl *DSL) Parse() { +// Parse Layout +func (dsl *DSL) Parse() error { + // init if dsl.Fields == nil { dsl.Fields = &FieldsDSL{ Filter: map[string]FilterFiledsDSL{}, Table: map[string]ViewFiledsDSL{}, } } + return dsl.parseProps() +} - for name, field := range dsl.Fields.Table { +// Xgen trans to xgen setting +func (dsl *DSL) Xgen() (map[string]interface{}, error) { - if field.In != "" { - dsl.ComputesIn[field.Bind] = field.In - dsl.ComputesIn[name] = field.In - } + setting, err := dsl.Layout.Xgen() + if err != nil { + return nil, err + } - if field.Out != "" { - dsl.ComputesOut[field.Bind] = field.Out - dsl.ComputesOut[name] = field.Out + 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/table/%s/component/%s/%s", dsl.ID, cProp.Xpath, cProp.Name), + "params": cProp.Query, + } + }) + + if err != nil { + return nil, err } } + 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 55fbb62b..c525ee32 100644 --- a/widgets/table/types.go +++ b/widgets/table/types.go @@ -4,15 +4,14 @@ import "github.com/yaoapp/yao/widgets/component" // 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:"-"` - Components map[string]*component.DSL `json:"-"` - Filters map[string]*component.DSL `json:"-"` + 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:"-"` } // ActionDSL the table action DSL @@ -94,14 +93,14 @@ type LayoutDSL struct { // HeaderLayoutDSL layout.header type HeaderLayoutDSL struct { - Preset PresetHeaderDSL `json:"preset,omitempty"` + Preset *PresetHeaderDSL `json:"preset,omitempty"` Actions []component.ActionDSL `json:"actions,omitempty"` } // PresetHeaderDSL layout.header.preset type PresetHeaderDSL struct { - Batch BatchPresetDSL `json:"batch,omitempty"` - Import ImportPresetDSL `json:"import,omitempty"` + Batch *BatchPresetDSL `json:"batch,omitempty"` + Import *ImportPresetDSL `json:"import,omitempty"` } // BatchPresetDSL layout.header.preset.batch