Merge pull request #277 from trheyi/main

[add] Widget List
This commit is contained in:
Max 2022-12-02 12:35:46 +08:00 committed by GitHub
commit 1c1b551715
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1463 additions and 7 deletions

View file

@ -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

View file

@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
go: [1.19.2]
runs-on: "ubuntu-latest"
runs-on: "ubuntu-20.04"
steps:
- name: Arm Build
run: |

View file

@ -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

View file

@ -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
}

View file

@ -13,6 +13,7 @@ import (
"github.com/yaoapp/yao/i18n"
"github.com/yaoapp/yao/runtime"
"github.com/yaoapp/yao/script"
_ "github.com/yaoapp/yao/utils"
"github.com/yaoapp/yao/widgets/login"
)
@ -224,7 +225,10 @@ func TestProcessMenu(t *testing.T) {
if err != nil {
t.Fatal(err)
}
assert.Len(t, res, 3)
data := any.Of(res).MapStr()
assert.True(t, data.Has("items"))
assert.True(t, data.Has("setting"))
}
func TestProcessIcons(t *testing.T) {

View file

@ -23,7 +23,7 @@ func TestLoad(t *testing.T) {
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 9, len(Forms))
assert.Equal(t, 10, len(Forms))
}
func prepare(t *testing.T, language ...string) {

93
widgets/list/action.go Normal file
View file

@ -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
}

162
widgets/list/api.go Normal file
View file

@ -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
}

85
widgets/list/bind.go Normal file
View file

@ -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)
}

61
widgets/list/compute.go Normal file
View file

@ -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
}

7
widgets/list/export.go Normal file
View file

@ -0,0 +1,7 @@
package list
// Export process & api
func Export() error {
exportProcess()
return exportAPI()
}

121
widgets/list/fields.go Normal file
View file

@ -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
}

121
widgets/list/handler.go Normal file
View file

@ -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
}

135
widgets/list/layout.go Normal file
View file

@ -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
}

252
widgets/list/list.go Normal file
View file

@ -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
}

81
widgets/list/list_test.go Normal file
View file

@ -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)
}
}

158
widgets/list/process.go Normal file
View file

@ -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)
}

View file

@ -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)
}
}
}

69
widgets/list/types.go Normal file
View file

@ -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"`
}

6
widgets/list/vaildate.go Normal file
View file

@ -0,0 +1,6 @@
package list
// Validate table
func (dsl *DSL) Validate() error {
return nil
}

View file

@ -70,7 +70,7 @@ func TestAPISearch(t *testing.T) {
}
data := any.Of(resp).MapStr().Dot()
assert.Equal(t, "20", fmt.Sprintf("%v", data.Get("pagesize")))
assert.Equal(t, "1", fmt.Sprintf("%v", data.Get("pagesize")))
assert.Equal(t, "3", fmt.Sprintf("%v", data.Get("total")))
assert.Equal(t, "checked", data.Get("data.0.status"))
assert.Equal(t, "enabled", data.Get("data.0.mode"))

View file

@ -11,6 +11,7 @@ import (
"github.com/yaoapp/yao/model"
"github.com/yaoapp/yao/runtime"
"github.com/yaoapp/yao/script"
_ "github.com/yaoapp/yao/utils"
"github.com/yaoapp/yao/widgets/app"
"github.com/yaoapp/yao/widgets/component"
"github.com/yaoapp/yao/widgets/expression"
@ -24,7 +25,7 @@ func TestLoad(t *testing.T) {
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 11, len(Tables))
assert.Equal(t, 12, len(Tables))
}
func TestLoadID(t *testing.T) {

View file

@ -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 {