From 0a2677606577188d35cb6b3ceb11612a70d4b4e5 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 2 Dec 2022 06:14:50 +0800 Subject: [PATCH] [add] Widget List --- .github/workflows/pr-test.yml | 2 +- .github/workflows/unit-test.yml | 2 +- service/guard.go | 2 + widgets/list/action.go | 93 ++++++++++++ widgets/list/api.go | 162 ++++++++++++++++++++ widgets/list/bind.go | 85 +++++++++++ widgets/list/compute.go | 61 ++++++++ widgets/list/export.go | 7 + widgets/list/fields.go | 121 +++++++++++++++ widgets/list/handler.go | 121 +++++++++++++++ widgets/list/layout.go | 135 +++++++++++++++++ widgets/list/list.go | 252 ++++++++++++++++++++++++++++++++ widgets/list/list_test.go | 81 ++++++++++ widgets/list/process.go | 158 ++++++++++++++++++++ widgets/list/process_test.go | 91 ++++++++++++ widgets/list/types.go | 69 +++++++++ widgets/list/vaildate.go | 6 + widgets/widgets.go | 7 + 18 files changed, 1453 insertions(+), 2 deletions(-) create mode 100644 widgets/list/action.go create mode 100644 widgets/list/api.go create mode 100644 widgets/list/bind.go create mode 100644 widgets/list/compute.go create mode 100644 widgets/list/export.go create mode 100644 widgets/list/fields.go create mode 100644 widgets/list/handler.go create mode 100644 widgets/list/layout.go create mode 100644 widgets/list/list.go create mode 100644 widgets/list/list_test.go create mode 100644 widgets/list/process.go create mode 100644 widgets/list/process_test.go create mode 100644 widgets/list/types.go create mode 100644 widgets/list/vaildate.go diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 5ed4626a..112a2830 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -140,7 +140,7 @@ jobs: - name: Checkout Demo App uses: actions/checkout@v2 with: - repository: yaoapp/demo-app + repository: yaoapp/yao-dev-app path: app - name: Move Kun, Xun, Gou, V8Go diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 29545dec..c5fdebe9 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -97,7 +97,7 @@ jobs: - name: Checkout Demo App uses: actions/checkout@v2 with: - repository: yaoapp/demo-app + repository: yaoapp/yao-dev-app path: app - name: Move Kun, Xun, Gou, V8Go diff --git a/service/guard.go b/service/guard.go index 42589a74..4f01c941 100644 --- a/service/guard.go +++ b/service/guard.go @@ -9,6 +9,7 @@ import ( "github.com/yaoapp/yao/widgets/chart" "github.com/yaoapp/yao/widgets/form" + "github.com/yaoapp/yao/widgets/list" "github.com/yaoapp/yao/widgets/table" ) @@ -18,6 +19,7 @@ var Guards = map[string]gin.HandlerFunc{ "cross-origin": guardCrossOrigin, // Cross-Origin Resource Sharing "table-guard": table_v0.Guard, // Table Guard ( v0.9 table) "widget-table": table.Guard, // Widget Table Guard + "widget-list": list.Guard, // Widget List Guard "widget-form": form.Guard, // Widget Form Guard "widget-chart": chart.Guard, // Widget Chart Guard } diff --git a/widgets/list/action.go b/widgets/list/action.go new file mode 100644 index 00000000..a432ba2d --- /dev/null +++ b/widgets/list/action.go @@ -0,0 +1,93 @@ +package list + +import ( + "github.com/yaoapp/gou" + "github.com/yaoapp/yao/widgets/action" + "github.com/yaoapp/yao/widgets/hook" + "github.com/yaoapp/yao/widgets/table" +) + +var processActionDefaults = map[string]*action.Process{ + + "Setting": { + Name: "yao.list.Setting", + Guard: "bearer-jwt", + Process: "yao.list.Xgen", + Default: []interface{}{nil}, + }, + "Component": { + Name: "yao.list.Component", + Guard: "bearer-jwt", + Default: []interface{}{nil, nil, nil}, + }, + "Upload": { + Name: "yao.list.Upload", + Guard: "bearer-jwt", + Default: []interface{}{nil, nil, nil}, + }, + "Download": { + Name: "yao.list.Download", + Guard: "-", + Process: "fs.system.Download", + Default: []interface{}{nil}, + }, + "Get": { + Name: "yao.list.Get", + Guard: "bearer-jwt", + Default: []interface{}{nil}, + }, + "Save": { + Name: "yao.list.Save", + Guard: "bearer-jwt", + Default: []interface{}{nil}, + }, +} + +// SetDefaultProcess set the default value of action +func (act *ActionDSL) SetDefaultProcess() { + + act.Setting = action.ProcessOf(act.Setting). + Merge(processActionDefaults["Setting"]). + SetHandler(processHandler) + + act.Component = action.ProcessOf(act.Component). + Merge(processActionDefaults["Component"]). + SetHandler(processHandler) + + act.Upload = action.ProcessOf(act.Upload). + Merge(processActionDefaults["Upload"]). + SetHandler(processHandler) + + act.Download = action.ProcessOf(act.Download). + Merge(processActionDefaults["Download"]). + SetHandler(processHandler) + + act.Save = action.ProcessOf(act.Save). + WithBefore(act.BeforeSave).WithAfter(act.AfterSave). + Merge(processActionDefaults["Save"]). + SetHandler(processHandler) + + act.Get = action.ProcessOf(act.Get). + WithBefore(act.BeforeSave).WithAfter(act.AfterSave). + Merge(processActionDefaults["Get"]). + SetHandler(processHandler) +} + +// BindModel bind model +func (act *ActionDSL) BindModel(m *gou.Model) error { + return nil +} + +// BindTable bind table +func (act *ActionDSL) BindTable(tab *table.DSL) error { + + // Copy Hooks + hook.CopyBefore(act.BeforeSave, tab.Action.BeforeSave) + + hook.CopyAfter(act.AfterSave, tab.Action.AfterSave) + + // Merge Actions + act.Save.Merge(tab.Action.Save) + + return nil +} diff --git a/widgets/list/api.go b/widgets/list/api.go new file mode 100644 index 00000000..f4776a1a --- /dev/null +++ b/widgets/list/api.go @@ -0,0 +1,162 @@ +package list + +import ( + "fmt" + + "github.com/gin-gonic/gin" + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/widgets/action" +) + +// Guard list widget guard +func Guard(c *gin.Context) { + + id := c.Param("id") + if id == "" { + abort(c, 400, "the list widget id does not found") + return + } + + list, has := Lists[id] + if !has { + abort(c, 404, fmt.Sprintf("the list widget %s does not exist", id)) + return + } + + act, err := list.getAction(c.FullPath()) + if err != nil { + abort(c, 404, err.Error()) + return + } + + err = act.UseGuard(c, id) + if err != nil { + abort(c, 400, err.Error()) + return + } + +} + +func abort(c *gin.Context, code int, message string) { + c.JSON(code, gin.H{"code": code, "message": message}) + c.Abort() +} + +func (list *DSL) getAction(path string) (*action.Process, error) { + + switch path { + case "/api/__yao/list/:id/setting": + return list.Action.Setting, nil + case "/api/__yao/list/:id/component/:xpath/:method": + return list.Action.Component, nil + case "/api/__yao/list/:id/upload/:xpath/:method": + return list.Action.Upload, nil + case "/api/__yao/list/:id/download/:field": + return list.Action.Download, nil + case "/api/__yao/list/:id/save": + return list.Action.Save, nil + } + + return nil, fmt.Errorf("the list widget %s %s action does not exist", list.ID, path) +} + +// export API +func exportAPI() error { + + http := gou.HTTP{ + Name: "Widget List API", + Description: "Widget List API", + Version: share.VERSION, + Guard: "widget-list", + Group: "__yao/list", + Paths: []gou.Path{}, + } + + // GET /api/__yao/list/:id/setting -> Default process: yao.list.Xgen + path := gou.Path{ + Label: "Setting", + Description: "Setting", + Path: "/:id/setting", + Method: "GET", + Process: "yao.list.Setting", + In: []string{"$param.id"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // GET /api/__yao/list/:id/find -> Default process: yao.list.Get $param.id :query + path = gou.Path{ + Label: "Get", + Description: "Get", + Path: "/:id/get", + Method: "GET", + Process: "yao.list.Find", + In: []string{"$param.id", ":query-param"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // GET /api/__yao/list/:id/component/:xpath/:method -> Default process: yao.list.Component $param.id $param.xpath $param.method :query + path = gou.Path{ + Label: "Component", + Description: "Component", + Path: "/:id/component/:xpath/:method", + Method: "GET", + Process: "yao.list.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/table/:id/upload/:xpath/:method -> Default process: yao.list.Upload $param.id $param.xpath $param.method $file.file + path = gou.Path{ + Label: "Upload", + Description: "Upload", + Path: "/:id/upload/:xpath/:method", + Method: "POST", + Process: "yao.list.Upload", + In: []string{"$param.id", "$param.xpath", "$param.method", "$file.file"}, + Out: gou.Out{Status: 200, Type: "application/json"}, + } + http.Paths = append(http.Paths, path) + + // GET /api/__yao/list/:id/download/:field -> Default process: yao.list.Download $param.id $param.xpath $param.field $query.name $query.token + path = gou.Path{ + Label: "Download", + Description: "Download", + Path: "/:id/download/:field", + Method: "GET", + Process: "yao.list.Download", + In: []string{"$param.id", "$param.field", "$query.name", "$query.token"}, + Out: gou.Out{ + Status: 200, + Body: "{{content}}", + Headers: map[string]string{"Content-Type": "{{type}}"}, + }, + } + http.Paths = append(http.Paths, path) + + // POST /api/__yao/list/:id/save -> Default process: yao.list.Save $param.id :payload + path = gou.Path{ + Label: "Save", + Description: "Save", + Path: "/:id/save", + Method: "POST", + Process: "yao.list.Save", + In: []string{"$param.id", ":payload"}, + 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.list") + return err +} diff --git a/widgets/list/bind.go b/widgets/list/bind.go new file mode 100644 index 00000000..586b4ff2 --- /dev/null +++ b/widgets/list/bind.go @@ -0,0 +1,85 @@ +package list + +import ( + "fmt" + + "github.com/yaoapp/gou" + "github.com/yaoapp/yao/widgets/table" +) + +// 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, dsl.ID, dsl.Fields, dsl.Action.Bind.Option) + return nil +} + +func (dsl *DSL) bindTable() error { + id := dsl.Action.Bind.Table + + // Load table + if _, has := table.Tables[id]; !has { + if err := table.LoadID(id, dsl.Root); err != nil { + return err + } + } + + tab, err := table.Get(id) + if err != nil { + return err + } + + // Bind Fields + err = dsl.Fields.BindTable(tab) + if err != nil { + return err + } + + // Bind Actions + err = dsl.Action.BindTable(tab) + if err != nil { + return err + } + + // Bind Layout + err = dsl.Layout.BindTable(tab, dsl.ID, dsl.Fields) + if err != nil { + return err + } + + return nil +} + +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/list/compute.go b/widgets/list/compute.go new file mode 100644 index 00000000..4f4dd613 --- /dev/null +++ b/widgets/list/compute.go @@ -0,0 +1,61 @@ +package list + +import ( + "fmt" + + "github.com/yaoapp/yao/widgets/compute" + "github.com/yaoapp/yao/widgets/field" +) + +func (dsl *DSL) getField() func(string) (*field.ColumnDSL, string, string, error) { + return func(name string) (*field.ColumnDSL, string, string, error) { + field, has := dsl.Fields.List[name] + if !has { + return nil, "fields.list", dsl.ID, fmt.Errorf("fields.list.%s does not exist", name) + } + return &field, "fields.list", dsl.ID, nil + } +} + +func (dsl *DSL) computeMapping() error { + if dsl.Computes == nil { + dsl.Computes = &compute.Maps{ + Filter: map[string][]compute.Unit{}, + Edit: map[string][]compute.Unit{}, + View: map[string][]compute.Unit{}, + } + } + + if dsl.Fields == nil { + return nil + } + + if dsl.Fields.List != nil && dsl.Layout.List != nil { + + for _, inst := range dsl.Layout.List.Columns { + + if field, has := dsl.Fields.List[inst.Name]; has { + + // View + if field.View != nil && field.View.Compute != nil { + bind := field.ViewBind() + if _, has := dsl.Computes.View[bind]; !has { + dsl.Computes.View[bind] = []compute.Unit{} + } + dsl.Computes.View[bind] = append(dsl.Computes.View[bind], compute.Unit{Name: inst.Name, Kind: compute.View}) + } + + // Edit + if field.Edit != nil && field.Edit.Compute != nil { + bind := field.EditBind() + if _, has := dsl.Computes.Edit[bind]; !has { + dsl.Computes.Edit[bind] = []compute.Unit{} + } + dsl.Computes.Edit[bind] = append(dsl.Computes.Edit[bind], compute.Unit{Name: inst.Name, Kind: compute.Edit}) + } + } + } + } + + return nil +} diff --git a/widgets/list/export.go b/widgets/list/export.go new file mode 100644 index 00000000..83cfad5b --- /dev/null +++ b/widgets/list/export.go @@ -0,0 +1,7 @@ +package list + +// Export process & api +func Export() error { + exportProcess() + return exportAPI() +} diff --git a/widgets/list/fields.go b/widgets/list/fields.go new file mode 100644 index 00000000..648f8e0b --- /dev/null +++ b/widgets/list/fields.go @@ -0,0 +1,121 @@ +package list + +import ( + "fmt" + "strings" + + "github.com/yaoapp/gou" + "github.com/yaoapp/yao/widgets/table" +) + +// BindModel bind model +func (fields *FieldsDSL) BindModel(m *gou.Model) error { + + // fields.listMap = map[string]field.ColumnDSL{} + + // trans, err := field.ModelTranslist() + // if err != nil { + // return err + // } + + // for _, col := range m.Columns { + // data := col.Map() + // listField, err := trans.List(col.Type, data) + // if err != nil { + // return err + // } + + // // append columns + // if _, has := fields.List[listField.Key]; !has { + // fields.List[listField.Key] = *listField + + // // PASSWORD Fields + // if col.Crypt == "PASSWORD" { + // if fields.List[listField.Key].View != nil { + // fields.List[listField.Key].View.Compute = &component.Compute{ + // Process: "Hide", + // Args: []component.CArg{component.NewExp("value")}, + // } + // } + + // if fields.List[listField.Key].Edit != nil { + // fields.List[listField.Key].Edit.Props["type"] = "password" + // } + // } + // fields.listMap[col.Name] = fields.List[listField.Key] + // } + // } + + // return nil + return nil +} + +// BindTable bind table +func (fields *FieldsDSL) BindTable(tab *table.DSL) error { + + return nil + + // Bind tab + // if fields.List == nil || len(fields.List) == 0 { + // fields.List = field.Columns{} + // fields.listMap = map[string]field.ColumnDSL{} + // } + + // if tab.Fields.Table != nil { + // for key, list := range tab.Fields.Table { + // if list.Edit == nil { + // continue + // } + + // if _, has := fields.List[key]; !has { + // edit := *list.Edit + // fields.List[key] = field.ColumnDSL{Key: key, Bind: list.Bind, Edit: &edit} + // } + // } + + // mapping := tab.Fields.TableMap() + // for name, list := range mapping { + // if _, has := fields.listMap[name]; !has { + // if list.Edit == nil { + // continue + // } + // fields.listMap[name] = fields.List[name] + // } + // } + + // } + // return nil +} + +// Xgen trans to xgen setting +func (fields *FieldsDSL) Xgen(layout *LayoutDSL) (map[string]interface{}, error) { + res := map[string]interface{}{} + lists := map[string]interface{}{} + messages := []string{} + if layout.List != nil && layout.List.Columns != nil { + + for i, f := range layout.List.Columns { + name := f.Name + field, has := fields.List[name] + if !has { + if strings.HasPrefix(f.Name, "::") { + name = fmt.Sprintf("$L(%s)", strings.TrimPrefix(f.Name, "::")) + if field, has = fields.List[name]; has { + lists[name] = field.Map() + continue + } + } + + path := fmt.Sprintf("layout.columns[%d]", i) + messages = append(messages, fmt.Sprintf("fields.list.%s not found, checking %s", f.Name, path)) + } + lists[name] = field.Map() + } + } + + if len(messages) > 0 { + return nil, fmt.Errorf(strings.Join(messages, ";\n")) + } + res["list"] = lists + return res, nil +} diff --git a/widgets/list/handler.go b/widgets/list/handler.go new file mode 100644 index 00000000..fd9f5091 --- /dev/null +++ b/widgets/list/handler.go @@ -0,0 +1,121 @@ +package list + +import ( + "fmt" + "strings" + + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/i18n" + "github.com/yaoapp/yao/widgets/action" +) + +// ******************************** +// * Execute the process of list * +// ******************************** +// Life-Circle: Before Hook → Compute Edit → Run Process → Compute View → After Hook +// Execute Compute Edit On: Save +// Execute Compute View On: Get +func processHandler(p *action.Process, process *gou.Process) (interface{}, error) { + + list, 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("[list] %s %s process is required", list.ID, p.Name) + return nil, fmt.Errorf("[list] %s %s process is required", list.ID, p.Name) + } + + // Before Hook + if p.Before != nil { + log.Trace("[list] %s %s before: exec(%v)", list.ID, p.Name, args) + newArgs, err := p.Before.Exec(args, process.Sid, process.Global) + if err != nil { + log.Error("[list] %s %s before: %s", list.ID, p.Name, err.Error()) + } else { + log.Trace("[list] %s %s before: args:%v", list.ID, p.Name, args) + args = newArgs + } + } + + // Compute Edit + err = list.ComputeEdit(p.Name, process, args, list.getField()) + if err != nil { + log.Error("[list] %s %s Compute Edit Error: %s", list.ID, p.Name, err.Error()) + } + + // Execute Process + act, err := gou.ProcessOf(name, args...) + if err != nil { + log.Error("[list] %s %s -> %s %s", list.ID, p.Name, name, err.Error()) + return nil, fmt.Errorf("[list] %s %s -> %s %s", list.ID, p.Name, name, err.Error()) + } + + res, err := act.WithGlobal(process.Global).WithSID(process.Sid).Exec() + if err != nil { + log.Error("[list] %s %s -> %s %s", list.ID, p.Name, name, err.Error()) + return nil, fmt.Errorf("[list] %s %s -> %s %s", list.ID, p.Name, name, err.Error()) + } + + // Compute View + err = list.ComputeView(p.Name, process, res, list.getField()) + if err != nil { + log.Error("[list] %s %s Compute View Error: %s", list.ID, p.Name, err.Error()) + } + + // After hook + if p.After != nil { + log.Trace("[list] %s %s after: exec(%v)", list.ID, p.Name, res) + newRes, err := p.After.Exec(res, process.Sid, process.Global) + if err != nil { + log.Error("[list] %s %s after: %s", list.ID, p.Name, err.Error()) + } else { + log.Trace("[list] %s %s after: %v", list.ID, p.Name, newRes) + res = newRes + } + } + + // Tranlate the result + newRes, err := list.translate(p.Name, process, res) + if err != nil { + return nil, fmt.Errorf("[list] %s %s Translate Error: %s", list.ID, p.Name, err.Error()) + } + + return newRes, nil +} + +// translateSetting +func (dsl *DSL) translate(name string, process *gou.Process, data interface{}) (interface{}, error) { + + if strings.ToLower(name) != "yao.list.setting" { + return data, nil + } + + widgets := []string{} + if dsl.Action.Bind.Model != "" { + m := gou.Select(dsl.Action.Bind.Model) + widgets = append(widgets, fmt.Sprintf("model.%s", m.ID)) + } + + if dsl.Action.Bind.Table != "" { + widgets = append(widgets, fmt.Sprintf("table.%s", dsl.Action.Bind.Table)) + } + + widgets = append(widgets, fmt.Sprintf("list.%s", dsl.ID)) + res, err := i18n.Trans(process.Lang(config.Conf.Lang), widgets, data) + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/widgets/list/layout.go b/widgets/list/layout.go new file mode 100644 index 00000000..fbe123a0 --- /dev/null +++ b/widgets/list/layout.go @@ -0,0 +1,135 @@ +package list + +import ( + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/yao/widgets/table" +) + +// BindModel bind model +func (layout *LayoutDSL) BindModel(m *gou.Model, listID string, fields *FieldsDSL, option map[string]interface{}) { + // if layout.Primary == "" { + // layout.Primary = m.PrimaryKey + // } + + // if layout.Operation == nil { + // layout.Operation = &OperationLayoutDSL{ + // Preset: map[string]map[string]interface{}{"save": {}, "back": {}}, + // Actions: []component.ActionDSL{ + // { + // Title: "::Delete", + // Icon: "icon-trash-2", + // Style: "danger", + // Action: map[string]component.ParamsDSL{ + // "List.delete": {"model": listID}, + // }, + // Confirm: &component.ConfirmActionDSL{ + // Title: "::Confirm", + // Desc: "::Please confirm, the data cannot be recovered", + // }, + // }, + // }, + // } + // } + + // if layout.List == nil && len(fields.List) > 0 { + // layout.List = &ViewLayoutDSL{ + // Props: component.PropsDSL{}, + // Sections: []SectionDSL{{Columns: []Column{}}}, + // } + + // columns := []Column{} + // for _, namev := range m.ColumnNames { + // name, ok := namev.(string) + // if ok && name != "deleted_at" { + // if col, has := fields.listMap[name]; has { + // width := 12 + // if col.Edit != nil && (col.Edit.Type == "TextArea" || col.Edit.Type == "Upload") { + // width = 24 + // } + // // if c, has := m.Columns[name]; has { + // // typ := strings.ToLower(c.Type) + // // if typ == "id" || strings.Contains(typ, "integer") || strings.Contains(typ, "float") { + // // width = 6 + // // } + // // } + // columns = append(columns, Column{InstanceDSL: component.InstanceDSL{Name: col.Key, Width: width}}) + // } + // } + // } + // layout.List.Sections = []SectionDSL{{Columns: columns}} + // } +} + +// BindTable bind table +func (layout *LayoutDSL) BindTable(tab *table.DSL, listID string, fields *FieldsDSL) error { + + // if layout.Primary == "" { + // layout.Primary = tab.Layout.Primary + // } + + // if layout.Operation == nil { + // layout.Operation = &OperationLayoutDSL{ + // Preset: map[string]map[string]interface{}{"save": {}, "back": {}}, + // Actions: []component.ActionDSL{ + // { + // Title: "::Delete", + // Icon: "icon-trash-2", + // Style: "danger", + // Action: map[string]component.ParamsDSL{ + // "List.delete": {"model": listID}, + // }, + // Confirm: &component.ConfirmActionDSL{ + // Title: "::Confirm", + // Desc: "::Please confirm, the data cannot be recovered", + // }, + // }, + // }, + // } + // } + + // if layout.List == nil && + // tab.Layout != nil && tab.Layout.Table != nil && tab.Layout.Table.Columns != nil && + // len(tab.Layout.Table.Columns) > 0 { + + // layout.List = &ViewLayoutDSL{ + // Props: component.PropsDSL{}, + // Sections: []SectionDSL{{Columns: []Column{}}}, + // } + + // columns := []Column{} + // for _, column := range tab.Fields.Table { + // if column.Edit == nil { + // continue + // } + + // name := column.Key + // if col, has := fields.List[name]; has && column.Bind != "deleted_at" { + // width := 12 + // if col.Edit != nil && (col.Edit.Type == "TextArea" || col.Edit.Type == "Upload") { + // width = 24 + // } + // columns = append(columns, Column{InstanceDSL: component.InstanceDSL{Name: col.Key, Width: width}}) + // } + // } + // layout.List.Sections = []SectionDSL{{Columns: columns}} + // } + + return nil +} + +// 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/list/list.go b/widgets/list/list.go new file mode 100644 index 00000000..63914fd1 --- /dev/null +++ b/widgets/list/list.go @@ -0,0 +1,252 @@ +package list + +import ( + "fmt" + "path/filepath" + "strings" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/widgets/component" + "github.com/yaoapp/yao/widgets/environment" + "github.com/yaoapp/yao/widgets/field" +) + +// +// API: +// GET /api/__yao/list/:id/setting -> Default process: yao.list.Xgen +// GET /api/__yao/list/:id/get -> Default process: yao.list.Get $param.id :query +// GET /api/__yao/list/:id/component/:xpath/:method -> Default process: yao.list.Component $param.id $param.xpath $param.method :query +// POST /api/__yao/list/:id/save -> Default process: yao.list.Save $param.id :payload +// GET /api/__yao/list/:id/upload/:xpath/:method -> Default process: yao.list.Upload $param.id $param.xpath $param.method $file.file +// GET /api/__yao/list/:id/download/:field -> Default process: yao.list.Download $param.id $param.field $query.name $query.token +// +// Process: +// yao.list.Setting Return the App DSL +// yao.list.Xgen Return the Xgen setting +// yao.list.Component Return the result defined in props.xProps +// yao.list.Upload Upload file defined in props +// yao.list.Download Download file defined in props +// yao.list.Get Return the query record +// yao.list.Save Save a record + +// +// Hook: +// before:get +// after:get +// before:save +// after:save + +// Lists the loaded list widgets +var Lists map[string]*DSL = map[string]*DSL{} + +// New create a new DSL +func New(id string) *DSL { + return &DSL{ + ID: id, + Fields: &FieldsDSL{List: field.Columns{}}, + Layout: &LayoutDSL{}, + CProps: field.CloudProps{}, + Config: map[string]interface{}{}, + } +} + +// LoadAndExport load list +func LoadAndExport(cfg config.Config) error { + err := Load(cfg) + if err != nil { + log.Error(err.Error()) + } + return Export() +} + +// Load load task +func Load(cfg config.Config) error { + var root = filepath.Join(cfg.Root, "lists") + 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, err := environment.ReadFile(filename) + if err != nil { + messages = append(messages, err.Error()) + return + } + + err = LoadData(data, id, filepath.Dir(dir)) + if err != nil { + messages = append(messages, err.Error()) + } + }) + + if len(messages) > 0 { + return fmt.Errorf(strings.Join(messages, ";\n")) + } + + return err +} + +// LoadID load via id +func LoadID(id string, root string) error { + dirs := strings.Split(id, ".") + name := fmt.Sprintf("%s.list.json", dirs[len(dirs)-1]) + elems := []string{root} + elems = append(elems, dirs[0:len(dirs)-1]...) + elems = append(elems, "lists", name) + filename := filepath.Join(elems...) + data, err := environment.ReadFile(filename) + if err != nil { + return fmt.Errorf("[List] LoadID %s root=%s %s", id, root, err.Error()) + } + return LoadData(data, id, root) +} + +// LoadData load via data +func LoadData(data []byte, id string, root string) error { + dsl := New(id) + dsl.Root = root + + err := jsoniter.Unmarshal(data, dsl) + if err != nil { + return fmt.Errorf("[List] LoadData %s %s", id, err.Error()) + } + + 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 / list / ... + err = dsl.Bind() + if err != nil { + return fmt.Errorf("[List] LoadData Bind %s %s", id, err.Error()) + } + + // Parse + err = dsl.Parse() + if err != nil { + return fmt.Errorf("[List] LoadData Parse %s %s", id, err.Error()) + } + + // Validate + err = dsl.Validate() + if err != nil { + return fmt.Errorf("[List] LoadData Validate %s %s", id, err.Error()) + } + + Lists[id] = dsl + return nil +} + +// Get list via process or id +func Get(list interface{}) (*DSL, error) { + id := "" + switch list.(type) { + case string: + id = list.(string) + case *gou.Process: + id = list.(*gou.Process).ArgsString(0) + default: + return nil, fmt.Errorf("%v type does not support", list) + } + + t, has := Lists[id] + if !has { + return nil, fmt.Errorf("%s does not exist", id) + } + return t, nil +} + +// MustGet Get list via process or id thow error +func MustGet(list interface{}) *DSL { + t, err := Get(list) + if err != nil { + exception.New(err.Error(), 400).Throw() + } + return t +} + +// Parse Layout +func (dsl *DSL) Parse() error { + + // ComputeFields + err := dsl.computeMapping() + if err != nil { + return err + } + + // Columns + return dsl.Fields.List.CPropsMerge(dsl.CProps, func(name string, kind string, column field.ColumnDSL) (xpath string) { + return fmt.Sprintf("fields.list.%s.%s.props", name, kind) + }) +} + +// Xgen trans to xgen setting +func (dsl *DSL) Xgen() (map[string]interface{}, error) { + + if dsl.Layout == nil { + dsl.Layout = &LayoutDSL{List: &ViewLayoutDSL{}} + } + + if dsl.Layout.List == nil { + dsl.Layout.List = &ViewLayoutDSL{} + } + + setting, err := dsl.Layout.Xgen() + if err != nil { + return nil, err + } + + fields, err := dsl.Fields.Xgen(dsl.Layout) + if err != nil { + return nil, err + } + + // full width default value + if _, has := dsl.Config["full"]; !has { + dsl.Config["full"] = true + } + + setting["fields"] = fields + setting["config"] = dsl.Config + for _, cProp := range dsl.CProps { + err := cProp.Replace(setting, func(cProp component.CloudPropsDSL) interface{} { + + if cProp.Type == "Upload" { + return fmt.Sprintf("/api/__yao/list/%s%s", dsl.ID, cProp.UploadPath()) + } + + return map[string]interface{}{ + "api": fmt.Sprintf("/api/__yao/list/%s%s", dsl.ID, cProp.Path()), + "params": cProp.Query, + } + }) + if err != nil { + return nil, err + } + } + + setting["name"] = dsl.Name + return setting, nil +} diff --git a/widgets/list/list_test.go b/widgets/list/list_test.go new file mode 100644 index 00000000..5595ce43 --- /dev/null +++ b/widgets/list/list_test.go @@ -0,0 +1,81 @@ +package list + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/fs" + "github.com/yaoapp/yao/i18n" + "github.com/yaoapp/yao/model" + "github.com/yaoapp/yao/runtime" + "github.com/yaoapp/yao/script" + "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/table" + "github.com/yaoapp/yao/widgets/expression" + "github.com/yaoapp/yao/widgets/field" + "github.com/yaoapp/yao/widgets/test" +) + +func TestLoad(t *testing.T) { + prepare(t) + err := Load(config.Conf) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 3, len(Lists)) +} + +func prepare(t *testing.T, language ...string) { + + runtime.Load(config.Conf) + err := test.LoadEngine(language...) + if err != nil { + t.Fatal(err) + } + + i18n.Load(config.Conf) + share.DBConnect(config.Conf.DB) // removed later + + // load fs + err = fs.Load(config.Conf) + if err != nil { + t.Fatal(err) + } + + // 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 field transform + err = field.LoadAndExport(config.Conf) + if err != nil { + t.Fatal(err) + } + + // load expression + err = expression.Export() + if err != nil { + t.Fatal(err) + } + + // load tables + err = table.Load(config.Conf) + if err != nil { + t.Fatal(err) + } + + // export + err = Export() + if err != nil { + t.Fatal(err) + } +} diff --git a/widgets/list/process.go b/widgets/list/process.go new file mode 100644 index 00000000..6bf8edbe --- /dev/null +++ b/widgets/list/process.go @@ -0,0 +1,158 @@ +package list + +import ( + "fmt" + "net/url" + "strings" + + "github.com/yaoapp/gou" + "github.com/yaoapp/gou/fs" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/yao/helper" +) + +// Export process +func exportProcess() { + gou.RegisterProcessHandler("yao.list.setting", processSetting) + gou.RegisterProcessHandler("yao.list.xgen", processXgen) + gou.RegisterProcessHandler("yao.list.component", processComponent) + gou.RegisterProcessHandler("yao.list.upload", processUpload) + gou.RegisterProcessHandler("yao.list.download", processDownload) + gou.RegisterProcessHandler("yao.list.save", processSave) +} + +func processXgen(process *gou.Process) interface{} { + + list := MustGet(process) + setting, err := list.Xgen() + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + return setting +} + +func processComponent(process *gou.Process) interface{} { + + process.ValidateArgNums(3) + list := MustGet(process) + xpath := process.ArgsString(1) + method := process.ArgsString(2) + key := fmt.Sprintf("%s.$%s", xpath, method) + + // get cloud props + cProp, has := list.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 processDownload(process *gou.Process) interface{} { + + process.ValidateArgNums(4) + list := MustGet(process) + field := process.ArgsString(1) + file := process.ArgsString(2) + tokenString := process.ArgsString(3) + + // checking + ext := fs.ExtName(file) + if _, has := fs.DownloadWhitelist[ext]; !has { + exception.New("%s.%s .%s file does not allow", 403, list.ID, field, ext).Throw() + } + + // Auth + tokenString = strings.TrimSpace(strings.TrimPrefix(tokenString, "Bearer ")) + if tokenString == "" { + exception.New("%s.%s No permission", 403, list.ID, field).Throw() + } + claims := helper.JwtValidate(tokenString) + + // Get Process name + name := "fs.system.Download" + if list.Action.Download.Process != "" { + name = list.Action.Download.Process + } + + // Create process + p, err := gou.ProcessOf(name, file) + if err != nil { + log.Error("[downalod] %s.%s %s", list.ID, field, err.Error()) + exception.New("[downalod] %s.%s %s", 400, list.ID, field, err.Error()).Throw() + } + + // Excute process + res, err := p.WithGlobal(process.Global).WithSID(claims.SID).Exec() + if err != nil { + log.Error("[downalod] %s.%s %s", list.ID, field, err.Error()) + exception.New("[downalod] %s.%s %s", 500, list.ID, field, err.Error()).Throw() + } + + return res +} + +func processUpload(process *gou.Process) interface{} { + + process.ValidateArgNums(4) + list := MustGet(process) + xpath := process.ArgsString(1) + method := process.ArgsString(2) + key := fmt.Sprintf("%s.$%s", xpath, method) + + // get cloud props + cProp, has := list.CProps[key] + if !has { + exception.New("%s does not exist", 400, key).Throw() + } + + // $file.file + tmpfile, ok := process.Args[3].(gou.UploadFile) + if !ok { + exception.New("parameters error: %v", 400, process.Args[3]).Throw() + } + + // execute upload + res, err := cProp.ExecUpload(process, tmpfile) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + if file, ok := res.(string); ok { + field := strings.TrimSuffix(xpath, ".edit.props") + file = fmt.Sprintf("/api/__yao/list/%s/download/%s?name=%s", list.ID, url.QueryEscape(field), file) + return file + } + + return res +} + +func processSetting(process *gou.Process) interface{} { + list := MustGet(process) + process.Args = append(process.Args, process.Args[0]) // listle name + return list.Action.Setting.MustExec(process) +} + +func processGet(process *gou.Process) interface{} { + list := MustGet(process) + return list.Action.Get.MustExec(process) +} + +func processSave(process *gou.Process) interface{} { + list := MustGet(process) + return list.Action.Save.MustExec(process) +} diff --git a/widgets/list/process_test.go b/widgets/list/process_test.go new file mode 100644 index 00000000..96c57404 --- /dev/null +++ b/widgets/list/process_test.go @@ -0,0 +1,91 @@ +package list + +import ( + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/yao/config" + q "github.com/yaoapp/yao/query" +) + +func TestProcessSetting(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{"category"} + res, err := gou.NewProcess("yao.list.Setting", args...).Exec() + if err != nil { + t.Fatal(err) + } + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "/api/__yao/list/category/component/fields.list."+url.QueryEscape("父类")+".edit.props.xProps/remote", data.Get("fields.list.父类.edit.props.xProps.remote.api")) +} + +func TestProcessXgen(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{"category"} + res, err := gou.NewProcess("yao.list.Xgen", args...).Exec() + if err != nil { + t.Fatal(err) + } + data := any.Of(res).MapStr().Dot() + assert.Equal(t, "/api/__yao/list/category/component/fields.list."+url.QueryEscape("父类")+".edit.props.xProps/remote", data.Get("fields.list.父类.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) { + category := gou.Select("category") + err := category.Insert( + []string{"name", "stock", "status", "rank"}, + [][]interface{}{ + {"机器人", 100, "启用", 1}, + {"运输车", 80, "启用", 2}, + {"货柜", 100, "停用", 3}, + }, + ) + if err != nil { + t.Fatal(err) + } +} + +func tempFile(t *testing.T) string { + file, err := os.CreateTemp("", "unit-test") + if err != nil { + t.Fatal(err) + } + defer file.Close() + + _, err = file.Write([]byte("HELLO")) + if err != nil { + t.Fatal(err) + } + + return file.Name() +} + +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/list/types.go b/widgets/list/types.go new file mode 100644 index 00000000..b7e1bfd2 --- /dev/null +++ b/widgets/list/types.go @@ -0,0 +1,69 @@ +package list + +import ( + "github.com/yaoapp/yao/widgets/action" + "github.com/yaoapp/yao/widgets/component" + "github.com/yaoapp/yao/widgets/compute" + "github.com/yaoapp/yao/widgets/field" + "github.com/yaoapp/yao/widgets/hook" +) + +// DSL the list DSL +type DSL struct { + ID string `json:"id,omitempty"` + Root string `json:"-"` + Name string `json:"name,omitempty"` + Action *ActionDSL `json:"action"` + Layout *LayoutDSL `json:"layout"` + Fields *FieldsDSL `json:"fields"` + Config map[string]interface{} `json:"config,omitempty"` + CProps field.CloudProps `json:"-"` + compute.Computable +} + +// ActionDSL the list action DSL +type ActionDSL struct { + Bind *BindActionDSL `json:"bind,omitempty"` + Setting *action.Process `json:"setting,omitempty"` + Component *action.Process `json:"component,omitempty"` + Upload *action.Process `json:"upload,omitempty"` + Download *action.Process `json:"download,omitempty"` + Get *action.Process `json:"get,omitempty"` + Save *action.Process `json:"save,omitempty"` + BeforeGet *hook.Before `json:"before:find,omitempty"` + AfterGet *hook.After `json:"after:find,omitempty"` + BeforeSave *hook.Before `json:"before:save,omitempty"` + AfterSave *hook.After `json:"after:save,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 list layout DSL +type LayoutDSL struct { + List *ViewLayoutDSL `json:"list,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 list fields DSL +type FieldsDSL struct { + List field.Columns `json:"list,omitempty"` + listMap map[string]field.ColumnDSL +} + +// ViewLayoutDSL layout.list +type ViewLayoutDSL struct { + Props component.PropsDSL `json:"props,omitempty"` + Columns []component.InstanceDSL `json:"columns,omitempty"` +} diff --git a/widgets/list/vaildate.go b/widgets/list/vaildate.go new file mode 100644 index 00000000..a67086d2 --- /dev/null +++ b/widgets/list/vaildate.go @@ -0,0 +1,6 @@ +package list + +// Validate table +func (dsl *DSL) Validate() error { + return nil +} diff --git a/widgets/widgets.go b/widgets/widgets.go index 783d04de..14b72576 100644 --- a/widgets/widgets.go +++ b/widgets/widgets.go @@ -11,6 +11,7 @@ import ( "github.com/yaoapp/yao/widgets/expression" "github.com/yaoapp/yao/widgets/field" "github.com/yaoapp/yao/widgets/form" + "github.com/yaoapp/yao/widgets/list" "github.com/yaoapp/yao/widgets/login" "github.com/yaoapp/yao/widgets/table" ) @@ -56,6 +57,12 @@ func Load(cfg config.Config) error { messages = append(messages, err.Error()) } + // list widget + err = list.LoadAndExport(cfg) + if err != nil { + messages = append(messages, err.Error()) + } + // form widget err = form.LoadAndExport(cfg) if err != nil {