From 72b9a8b62d3b4c4e899981db025f4528a036126d Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 13 Dec 2022 23:30:47 +0800 Subject: [PATCH 1/2] [add] widget list permission support --- widgets/form/form.go | 4 +- widgets/list/api.go | 12 ++++++ widgets/list/fields.go | 7 ++++ widgets/list/layout.go | 41 +++++++++++++++---- widgets/list/list.go | 52 ++++++++++++++----------- widgets/list/{compute.go => mapping.go} | 25 +++++++++++- widgets/list/process.go | 7 +++- widgets/list/process_test.go | 40 +++++++++++++++++++ widgets/list/types.go | 2 + 9 files changed, 155 insertions(+), 35 deletions(-) rename widgets/list/{compute.go => mapping.go} (70%) diff --git a/widgets/form/form.go b/widgets/form/form.go index f7f57ed1..34e24cbe 100644 --- a/widgets/form/form.go +++ b/widgets/form/form.go @@ -232,10 +232,10 @@ func (dsl *DSL) Xgen(data map[string]interface{}, excludes map[string]bool) (map return nil, err } - // Hooks - onChange := map[string]interface{}{} + onChange := map[string]interface{}{} // Hooks setting["fields"] = fields setting["config"] = dsl.Config + for _, cProp := range dsl.CProps { err := cProp.Replace(setting, func(cProp component.CloudPropsDSL) interface{} { diff --git a/widgets/list/api.go b/widgets/list/api.go index f4776a1a..1953dff3 100644 --- a/widgets/list/api.go +++ b/widgets/list/api.go @@ -110,6 +110,18 @@ func exportAPI() error { } http.Paths = append(http.Paths, path) + // POST /api/__yao/list/:id/component/:xpath/:method -> Default process: yao.list.Component $param.id $param.xpath $param.method :payload + path = gou.Path{ + Label: "Component", + Description: "Component", + Path: "/:id/component/:xpath/:method", + Method: "POST", + Process: "yao.list.Component", + In: []string{"$param.id", "$param.xpath", "$param.method", ":payload"}, + 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", diff --git a/widgets/list/fields.go b/widgets/list/fields.go index 648f8e0b..c9a662e9 100644 --- a/widgets/list/fields.go +++ b/widgets/list/fields.go @@ -109,6 +109,13 @@ func (fields *FieldsDSL) Xgen(layout *LayoutDSL) (map[string]interface{}, error) path := fmt.Sprintf("layout.columns[%d]", i) messages = append(messages, fmt.Sprintf("fields.list.%s not found, checking %s", f.Name, path)) } + + if field.Edit != nil && field.Edit.Props != nil { + if _, has := field.Edit.Props["$on:change"]; has { + delete(field.Edit.Props, "$on:change") + } + } + lists[name] = field.Map() } } diff --git a/widgets/list/layout.go b/widgets/list/layout.go index fbe123a0..012a9fee 100644 --- a/widgets/list/layout.go +++ b/widgets/list/layout.go @@ -3,6 +3,8 @@ package list import ( jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou" + "github.com/yaoapp/yao/widgets/component" + "github.com/yaoapp/yao/widgets/mapping" "github.com/yaoapp/yao/widgets/table" ) @@ -119,17 +121,42 @@ func (layout *LayoutDSL) BindTable(tab *table.DSL, listID string, fields *Fields } // Xgen trans to Xgen setting -func (layout *LayoutDSL) Xgen() (map[string]interface{}, error) { - res := map[string]interface{}{} - data, err := jsoniter.Marshal(layout) +func (layout *LayoutDSL) Xgen(data map[string]interface{}, excludes map[string]bool, mapping *mapping.Mapping) (*LayoutDSL, error) { + clone, err := layout.Clone() if err != nil { return nil, err } - err = jsoniter.Unmarshal(data, &res) - if err != nil { - return nil, err + // layout.list.columns + columns := []component.InstanceDSL{} + if clone.List != nil && clone.List.Columns != nil { + for _, column := range clone.List.Columns { + id, has := mapping.Columns[column.Name] + if !has { + continue + } + + if _, has := excludes[id]; has { + continue + } + columns = append(columns, column) + } + clone.List.Columns = columns } - return res, nil + return clone, nil +} + +// Clone layout for output +func (layout *LayoutDSL) Clone() (*LayoutDSL, error) { + new := LayoutDSL{} + bytes, err := jsoniter.Marshal(layout) + if err != nil { + return nil, err + } + err = jsoniter.Unmarshal(bytes, &new) + if err != nil { + return nil, err + } + return &new, nil } diff --git a/widgets/list/list.go b/widgets/list/list.go index 63914fd1..5f082ecc 100644 --- a/widgets/list/list.go +++ b/widgets/list/list.go @@ -143,10 +143,10 @@ func LoadData(data []byte, id string, root string) error { return fmt.Errorf("[List] LoadData Bind %s %s", id, err.Error()) } - // Parse - err = dsl.Parse() + // Mapping + err = dsl.mapping() if err != nil { - return fmt.Errorf("[List] LoadData Parse %s %s", id, err.Error()) + return fmt.Errorf("[List] LoadData Mapping %s %s", id, err.Error()) } // Validate @@ -187,23 +187,8 @@ func MustGet(list interface{}) *DSL { 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) { +func (dsl *DSL) Xgen(data map[string]interface{}, excludes map[string]bool) (map[string]interface{}, error) { if dsl.Layout == nil { dsl.Layout = &LayoutDSL{List: &ViewLayoutDSL{}} @@ -213,12 +198,12 @@ func (dsl *DSL) Xgen() (map[string]interface{}, error) { dsl.Layout.List = &ViewLayoutDSL{} } - setting, err := dsl.Layout.Xgen() + layout, err := dsl.Layout.Xgen(data, excludes, dsl.Mapping) if err != nil { return nil, err } - fields, err := dsl.Fields.Xgen(dsl.Layout) + fields, err := dsl.Fields.Xgen(layout) if err != nil { return nil, err } @@ -228,6 +213,18 @@ func (dsl *DSL) Xgen() (map[string]interface{}, error) { dsl.Config["full"] = true } + setting := map[string]interface{}{} + bytes, err := jsoniter.Marshal(layout) + if err != nil { + return nil, err + } + + err = jsoniter.Unmarshal(bytes, &setting) + if err != nil { + return nil, err + } + + onChange := map[string]interface{}{} // Hooks setting["fields"] = fields setting["config"] = dsl.Config for _, cProp := range dsl.CProps { @@ -242,11 +239,22 @@ func (dsl *DSL) Xgen() (map[string]interface{}, error) { "params": cProp.Query, } }) + if err != nil { return nil, err } - } + // hooks + if cProp.Name == "on:change" { + field := strings.TrimPrefix(cProp.Xpath, "fields.list.") + field = strings.TrimSuffix(field, ".edit.props") + onChange[field] = map[string]interface{}{ + "api": fmt.Sprintf("/api/__yao/list/%s%s", dsl.ID, cProp.Path()), + "params": cProp.Query, + } + } + } + setting["hooks"] = map[string]interface{}{"onChange": onChange} setting["name"] = dsl.Name return setting, nil } diff --git a/widgets/list/compute.go b/widgets/list/mapping.go similarity index 70% rename from widgets/list/compute.go rename to widgets/list/mapping.go index 4f4dd613..1c77cfb8 100644 --- a/widgets/list/compute.go +++ b/widgets/list/mapping.go @@ -5,6 +5,7 @@ import ( "github.com/yaoapp/yao/widgets/compute" "github.com/yaoapp/yao/widgets/field" + "github.com/yaoapp/yao/widgets/mapping" ) func (dsl *DSL) getField() func(string) (*field.ColumnDSL, string, string, error) { @@ -17,7 +18,7 @@ func (dsl *DSL) getField() func(string) (*field.ColumnDSL, string, string, error } } -func (dsl *DSL) computeMapping() error { +func (dsl *DSL) mapping() error { if dsl.Computes == nil { dsl.Computes = &compute.Maps{ Filter: map[string][]compute.Unit{}, @@ -26,16 +27,33 @@ func (dsl *DSL) computeMapping() error { } } + if dsl.Mapping == nil { + dsl.Mapping = &mapping.Mapping{} + } + + if dsl.Mapping.Filters == nil { + dsl.Mapping.Filters = map[string]string{} + } + + if dsl.Mapping.Columns == nil { + dsl.Mapping.Columns = map[string]string{} + } + if dsl.Fields == nil { return nil } + // Mapping compute and id 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 { + // Mapping ID + dsl.Mapping.Columns[field.ID] = inst.Name + dsl.Mapping.Columns[inst.Name] = field.ID + // View if field.View != nil && field.View.Compute != nil { bind := field.ViewBind() @@ -57,5 +75,8 @@ func (dsl *DSL) computeMapping() error { } } - return nil + // 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) + }) } diff --git a/widgets/list/process.go b/widgets/list/process.go index 6bf8edbe..b30ae9b2 100644 --- a/widgets/list/process.go +++ b/widgets/list/process.go @@ -10,6 +10,7 @@ import ( "github.com/yaoapp/kun/exception" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/helper" + "github.com/yaoapp/yao/widgets/app" ) // Export process @@ -25,7 +26,9 @@ func exportProcess() { func processXgen(process *gou.Process) interface{} { list := MustGet(process) - setting, err := list.Xgen() + data := process.ArgsMap(1, map[string]interface{}{}) + excludes := app.Permissions(process, "lists", list.ID) + setting, err := list.Xgen(data, excludes) if err != nil { exception.New(err.Error(), 500).Throw() } @@ -143,7 +146,7 @@ func processUpload(process *gou.Process) interface{} { func processSetting(process *gou.Process) interface{} { list := MustGet(process) - process.Args = append(process.Args, process.Args[0]) // listle name + process.Args = append(process.Args, process.Args[0]) // list name return list.Action.Setting.MustExec(process) } diff --git a/widgets/list/process_test.go b/widgets/list/process_test.go index 96c57404..9cb08240 100644 --- a/widgets/list/process_test.go +++ b/widgets/list/process_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/yaoapp/gou" + "github.com/yaoapp/gou/session" "github.com/yaoapp/kun/any" "github.com/yaoapp/yao/config" q "github.com/yaoapp/yao/query" @@ -23,6 +24,8 @@ func TestProcessSetting(t *testing.T) { } 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")) + assert.Equal(t, "/api/__yao/list/category/component/fields.list."+url.QueryEscape("名称")+".edit.props/on%3Achange", data.Get("hooks.onChange.名称.api")) + assert.Equal(t, "开发者自定义", data.Get("hooks.onChange.名称.params.extra")) } func TestProcessXgen(t *testing.T) { @@ -36,6 +39,43 @@ func TestProcessXgen(t *testing.T) { } 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")) + assert.Equal(t, "/api/__yao/list/category/component/fields.list."+url.QueryEscape("名称")+".edit.props/on%3Achange", data.Get("hooks.onChange.名称.api")) + assert.Equal(t, "开发者自定义", data.Get("hooks.onChange.名称.params.extra")) +} + +func TestProcessXgenWithPermissions(t *testing.T) { + load(t) + clear(t) + testData(t) + + session.Global().Set("__permissions", map[string]interface{}{ + "lists.category": []string{ + "a189b2bf0dd9b29f6628b386e501397f", // fields.list.库存预警 + }, + }) + + 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")) + assert.Equal(t, "/api/__yao/list/category/component/fields.list."+url.QueryEscape("名称")+".edit.props/on%3Achange", data.Get("hooks.onChange.名称.api")) + assert.Equal(t, "开发者自定义", data.Get("hooks.onChange.名称.params.extra")) + assert.False(t, data.Has("fields.list.库存预警")) + + session.Global().Set("__permissions", nil) + 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")) + assert.Equal(t, "/api/__yao/list/category/component/fields.list."+url.QueryEscape("名称")+".edit.props/on%3Achange", data.Get("hooks.onChange.名称.api")) + assert.Equal(t, "开发者自定义", data.Get("hooks.onChange.名称.params.extra")) + assert.True(t, data.Has("fields.list.库存预警")) + } func load(t *testing.T) { diff --git a/widgets/list/types.go b/widgets/list/types.go index b7e1bfd2..1dcd7044 100644 --- a/widgets/list/types.go +++ b/widgets/list/types.go @@ -6,6 +6,7 @@ import ( "github.com/yaoapp/yao/widgets/compute" "github.com/yaoapp/yao/widgets/field" "github.com/yaoapp/yao/widgets/hook" + "github.com/yaoapp/yao/widgets/mapping" ) // DSL the list DSL @@ -19,6 +20,7 @@ type DSL struct { Config map[string]interface{} `json:"config,omitempty"` CProps field.CloudProps `json:"-"` compute.Computable + *mapping.Mapping } // ActionDSL the list action DSL From bab61e8cbd4e1c79054f8674537f594b08674760 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 14 Dec 2022 00:18:41 +0800 Subject: [PATCH 2/2] [add] widget.fields & widget.filters --- widgets/action.go | 2 +- widgets/field.go | 145 ++++++++++++++++++++++++++++++++++++++++ widgets/field/column.go | 5 ++ widgets/field/filter.go | 5 ++ widgets/process.go | 12 ++++ widgets/process_test.go | 20 ++++++ 6 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 widgets/field.go diff --git a/widgets/action.go b/widgets/action.go index e2f1fd45..620db0ef 100644 --- a/widgets/action.go +++ b/widgets/action.go @@ -55,7 +55,7 @@ func widgetActions(actions map[string]interface{}, widget WidgetAction, widgetID items := widget.Actions() if len(items) > 0 { actions[dsl] = map[string]interface{}{ - "items": widget.Actions(), + "items": items, "DSL": dsl, "ID": widgetID, "name": name, diff --git a/widgets/field.go b/widgets/field.go new file mode 100644 index 00000000..877175a8 --- /dev/null +++ b/widgets/field.go @@ -0,0 +1,145 @@ +package widgets + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/yaoapp/yao/widgets/chart" + "github.com/yaoapp/yao/widgets/field" + "github.com/yaoapp/yao/widgets/form" + "github.com/yaoapp/yao/widgets/list" + "github.com/yaoapp/yao/widgets/table" +) + +// Fields return loaded widgets fields +func Fields() []Item { + + fields := map[string]interface{}{} + tableFields(fields) + formFields(fields) + listFields(fields) + chartFields(fields) + + grouping := Grouping(fields) + items := Array(grouping, []Item{}) + Sort(items, []string{"tables", "forms", "lists", "charts"}) + return items +} + +// Filters return loaded widgets filters +func Filters() []Item { + filters := map[string]interface{}{} + tableFilters(filters) + chartFilters(filters) + + grouping := Grouping(filters) + items := Array(grouping, []Item{}) + Sort(items, []string{"tables", "forms", "lists", "charts"}) + return items +} + +func tableFields(fields map[string]interface{}) { + for id, widget := range table.Tables { + dsl := fmt.Sprintf("tables%s%s.tab.json", string(os.PathSeparator), strings.ReplaceAll(id, ".", string(os.PathSeparator))) + widgetFields(fields, widget.Fields.Table, id, dsl, widget.Name) + } +} + +func formFields(fields map[string]interface{}) { + for id, widget := range form.Forms { + dsl := fmt.Sprintf("forms%s%s.form.json", string(os.PathSeparator), strings.ReplaceAll(id, ".", string(os.PathSeparator))) + widgetFields(fields, widget.Fields.Form, id, dsl, widget.Name) + } +} + +func chartFields(fields map[string]interface{}) { + for id, widget := range chart.Charts { + dsl := fmt.Sprintf("charts%s%s.chart.json", string(os.PathSeparator), strings.ReplaceAll(id, ".", string(os.PathSeparator))) + widgetFields(fields, widget.Fields.Chart, id, dsl, widget.Name) + } +} + +func listFields(fields map[string]interface{}) { + for id, widget := range list.Lists { + dsl := fmt.Sprintf("lists%s%s.list.json", string(os.PathSeparator), strings.ReplaceAll(id, ".", string(os.PathSeparator))) + widgetFields(fields, widget.Fields.List, id, dsl, widget.Name) + } +} + +func tableFilters(filters map[string]interface{}) { + for id, widget := range table.Tables { + dsl := fmt.Sprintf("tables%s%s.tab.json", string(os.PathSeparator), strings.ReplaceAll(id, ".", string(os.PathSeparator))) + widgetFilters(filters, widget.Fields.Filter, id, dsl, widget.Name) + } +} + +func chartFilters(filters map[string]interface{}) { + for id, widget := range chart.Charts { + dsl := fmt.Sprintf("charts%s%s.chart.json", string(os.PathSeparator), strings.ReplaceAll(id, ".", string(os.PathSeparator))) + widgetFilters(filters, widget.Fields.Filter, id, dsl, widget.Name) + } +} + +func widgetFields(items map[string]interface{}, fields map[string]field.ColumnDSL, widgetID string, dsl string, name string) map[string]interface{} { + + fieldlist := []map[string]string{} + if fields != nil { + names := []string{} + mapping := map[string]string{} + for name, field := range fields { + if field.ID != "" { + mapping[name] = field.ID + names = append(names, name) + } + } + sort.Strings(names) + for _, name := range names { + fieldlist = append(fieldlist, map[string]string{ + "name": name, + "id": mapping[name], + }) + } + } + + items[dsl] = map[string]interface{}{ + "items": fieldlist, + "DSL": dsl, + "ID": widgetID, + "name": name, + } + + return nil +} + +func widgetFilters(items map[string]interface{}, fields map[string]field.FilterDSL, widgetID string, dsl string, name string) map[string]interface{} { + + fieldlist := []map[string]string{} + if fields != nil { + names := []string{} + mapping := map[string]string{} + for name, field := range fields { + if field.ID != "" { + mapping[name] = field.ID + names = append(names, name) + } + } + sort.Strings(names) + for _, name := range names { + fieldlist = append(fieldlist, map[string]string{ + "name": name, + "id": mapping[name], + }) + } + } + + items[dsl] = map[string]interface{}{ + "items": fieldlist, + "DSL": dsl, + "ID": widgetID, + "name": name, + } + + return nil +} diff --git a/widgets/field/column.go b/widgets/field/column.go index 73741e34..3c659a33 100644 --- a/widgets/field/column.go +++ b/widgets/field/column.go @@ -63,6 +63,11 @@ func (column ColumnDSL) Replace(data map[string]interface{}) (*ColumnDSL, error) } } + new.ID, err = column.Hash() + if err != nil { + return nil, err + } + return &new, nil } diff --git a/widgets/field/filter.go b/widgets/field/filter.go index e8e98bf3..6d12f1f8 100644 --- a/widgets/field/filter.go +++ b/widgets/field/filter.go @@ -56,6 +56,11 @@ func (filter FilterDSL) Replace(data map[string]interface{}) (*FilterDSL, error) } } + new.ID, err = filter.Hash() + if err != nil { + return nil, err + } + return &new, nil } diff --git a/widgets/process.go b/widgets/process.go index 3a7c99c9..f65677e5 100644 --- a/widgets/process.go +++ b/widgets/process.go @@ -9,6 +9,8 @@ var WidgetHandlers = map[string]gou.ProcessHandler{ "apis": processApis, "actions": processActions, "models": processModels, + "fields": processFields, + "filters": processFilters, } func init() { @@ -30,6 +32,16 @@ func processModels(process *gou.Process) interface{} { return Models() } +// Get the loaded Fields +func processFields(process *gou.Process) interface{} { + return Fields() +} + +// Get the loaded Filters +func processFilters(process *gou.Process) interface{} { + return Filters() +} + // Get the loaded flows func processFlows() {} diff --git a/widgets/process_test.go b/widgets/process_test.go index 83010a4b..5ce3fd56 100644 --- a/widgets/process_test.go +++ b/widgets/process_test.go @@ -38,6 +38,26 @@ func TestProcessModels(t *testing.T) { assert.Greater(t, len(res.([]Item)), 0) } +func TestProcessFields(t *testing.T) { + testData(t) + args := []interface{}{} + res, err := gou.NewProcess("widget.fields", args...).Exec() + if err != nil { + t.Fatal(err) + } + assert.Greater(t, len(res.([]Item)), 0) +} + +func TestProcessFilters(t *testing.T) { + testData(t) + args := []interface{}{} + res, err := gou.NewProcess("widget.filters", args...).Exec() + if err != nil { + t.Fatal(err) + } + assert.Greater(t, len(res.([]Item)), 0) +} + func testData(t *testing.T) { prepare(t) err := Load(config.Conf)