diff --git a/engine/load.go b/engine/load.go index 7adb9167..c35bae19 100644 --- a/engine/load.go +++ b/engine/load.go @@ -27,6 +27,7 @@ import ( "github.com/yaoapp/yao/table" "github.com/yaoapp/yao/task" "github.com/yaoapp/yao/websocket" + "github.com/yaoapp/yao/widget" ) // Load 根据配置加载 API, FLow, Model, Plugin @@ -133,6 +134,11 @@ func Load(cfg config.Config) (err error) { log.Debug(err.Error()) } + err = widget.Load(cfg) // Load schedules + if err != nil { + log.Debug(err.Error()) + } + return nil } diff --git a/tests/dyforms/app.form.json b/tests/dyforms/app.form.json new file mode 100644 index 00000000..4e2b288d --- /dev/null +++ b/tests/dyforms/app.form.json @@ -0,0 +1,58 @@ +{ + "guard": "bearer-jwt", + "title": "应用表单", + "sn": "AP1024", + "orign": "手机应用", + "columns": [ + { + "name": "门店", + "field": "store_id", + "searchable": true, + "props": { + "value": ":store_id", + "type": "SelectStore", + "title": "门店", + "placeholder": "请选择门店", + "validation": [ + { "type": "Required" }, + { "type": "StoreStatus", "args": [":store_id"] } + ] + } + }, + { + "name": "订单数量", + "field": "orders_amount", + "searchable": true, + "props": { + "value": ":orders_amount", + "type": "Number", + "title": "订单数量", + "placeholder": "今日订单数量", + "computed": "OrderAmount", + "validation": [ + { "type": "Required" }, + { "type": "IsNumber", "args": [":orders_amount"] } + ] + } + }, + { + "name": "账单地址", + "field": "address", + "props": { + "value": ":address", + "type": "Text", + "title": "账单地址", + "placeholder": "填写账单地址", + "validation": [ + { "type": "Required" }, + { "type": "MaxLength", "args": [":address", 200] } + ] + } + } + ], + "filters": [], + "list-layout": { + "columns": [{ "name": "store_id" }, { "name": "orders_amount" }], + "filters": [{ "name": "订单数量" }] + } +} diff --git a/tests/dyforms/website.form.json b/tests/dyforms/website.form.json new file mode 100644 index 00000000..4dc5be25 --- /dev/null +++ b/tests/dyforms/website.form.json @@ -0,0 +1,77 @@ +{ + "guard": "bearer-jwt", + "title": "官网表单", + "sn": "AP2048", + "orign": "官方网站", + "columns": [ + { + "name": "门店", + "field": "store_id", + "searchable": true, + "props": { + "value": ":store_id", + "type": "SelectStore", + "title": "门店", + "placeholder": "请选择门店", + "validation": [ + { "type": "Required" }, + { "type": "StoreStatus", "args": [":store_id"] } + ] + } + }, + { + "name": "订单数量", + "field": "orders_amount", + "searchable": true, + "props": { + "value": ":orders_amount", + "type": "Number", + "title": "订单数量", + "placeholder": "今日订单数量", + "computed": "OrderAmount", + "validation": [ + { "type": "Required" }, + { "type": "IsNumber", "args": [":orders_amount"] } + ] + } + }, + { + "name": "账单地址", + "field": "address", + "props": { + "value": ":address", + "type": "Text", + "title": "账单地址", + "placeholder": "填写账单地址", + "validation": [ + { "type": "Required" }, + { "type": "MaxLength", "args": [":address", 200] } + ] + } + } + ], + "filters": [ + { + "订单数量": { + "label": "订单数量超过", + "bind": "where.orders_amount.ge", + "type": "Select", + "props": { + "placeholder": "订单数量", + "options": [ + { "label": "大于100", "value": 100 }, + { "label": "大于50", "value": 50 } + ] + } + } + } + ], + "list-layout": { + "columns": [ + { "name": "store_id" }, + { "name": "orders_amount" }, + { "name": "address" } + ], + "filters": [{ "name": "订单数量" }] + } +} diff --git a/tests/scripts/mail.js b/tests/scripts/mail.js index 978a5442..cc8a4fb4 100644 --- a/tests/scripts/mail.js +++ b/tests/scripts/mail.js @@ -12,12 +12,12 @@ function NextID() { function Send(id, mail, flag) { for (var i = 1; i <= 3; i++) { - Process("xiang.system.Sleep", 200); + Process("xiang.sys.Sleep", 200); Process("tasks.mail.Progress", id, i, 3, "unit-test"); } if (flag) { console.log(`flag: ${flag}`); - Process("xiang.system.Sleep", 2000); + Process("xiang.sys.Sleep", 2000); } console.log( `Send: ${JSON.stringify({ foo: "bar", mail: mail, flag: flag || "-" })}` diff --git a/tests/widgets/README.md b/tests/widgets/README.md new file mode 100644 index 00000000..18be262f --- /dev/null +++ b/tests/widgets/README.md @@ -0,0 +1,3 @@ +# Widgets + +## a customize low-code modules diff --git a/tests/widgets/dyform/README.md b/tests/widgets/dyform/README.md new file mode 100644 index 00000000..0c3894f7 --- /dev/null +++ b/tests/widgets/dyform/README.md @@ -0,0 +1,3 @@ +# DyForm + +## A form widget. users can design forms online diff --git a/tests/widgets/dyform/assets/README.md b/tests/widgets/dyform/assets/README.md new file mode 100644 index 00000000..226d49ab --- /dev/null +++ b/tests/widgets/dyform/assets/README.md @@ -0,0 +1,5 @@ +# Assets + +1. The assets folder's file will be copied to the `ui/.widgets//` folder. +2. If the folder does not exist will be created. To upgrade the assets, remove the `ui/.widgets//` folder. +3. The asset file PUBLIC URL is `http(s):///.widgets//`, ANYONE CAN VISIT DIRECTLY. diff --git a/tests/widgets/dyform/compile.js b/tests/widgets/dyform/compile.js new file mode 100644 index 00000000..2a4ec0b5 --- /dev/null +++ b/tests/widgets/dyform/compile.js @@ -0,0 +1,107 @@ +/** + * The DSL compiler. + * Translate the customize DSL to Models, Processes, Flows, Tables, etc. + */ + +/** + * Source + * Where to get the source of DSL + */ +function Source() { + return dyforms(); +} + +/** + * Compile + * Translate or extend the customize DSL + * @param {*} dsl + */ +function Compile(name, dsl) { + let newdsl = { name: name }; + return newdsl; +} + +/** + * OnLoad + * When the widget instance are loaded, the function will be called. + * For preparing the sources the widget need. + * @param {DSL} dsl + */ +function OnLoad(name, dsl) { + console.log(name, dsl); +} + +/** + * Migrate + * When the migrate command executes, the function will be called + * @param {DSL} dsl + * @param {bool} force + */ +function Migrate(dsl, force) {} + +/** + * Customize DSL + * @returns + */ +function dyforms() { + return { + pad: { + guard: "bearer-jwt", + title: "应用表单", + sn: "PAD1024", + orign: "手机平板", + columns: [ + { + name: "门店", + field: "store_id", + searchable: true, + props: { + value: ":store_id", + type: "SelectStore", + title: "门店", + placeholder: "请选择门店", + validation: [ + { type: "Required" }, + { type: "StoreStatus", args: [":store_id"] }, + ], + }, + }, + { + name: "订单数量", + field: "orders_amount", + searchable: true, + props: { + value: ":orders_amount", + type: "Number", + title: "订单数量", + placeholder: "今日订单数量", + computed: "OrderAmount", + validation: [ + { type: "Required" }, + { type: "IsNumber", args: [":orders_amount"] }, + ], + }, + }, + { + name: "账单地址", + field: "address", + props: { + value: ":address", + type: "Text", + title: "账单地址", + placeholder: "填写账单地址", + validation: [ + { type: "Required" }, + { type: "MaxLength", args: [":address", 200] }, + ], + }, + }, + ], + filters: [], + "list-layout": { + columns: [{ name: "store_id" }, { name: "orders_amount" }], + filters: [{ name: "订单数量" }], + }, + }, + }; +} diff --git a/tests/widgets/dyform/export.js b/tests/widgets/dyform/export.js new file mode 100644 index 00000000..40f4b658 --- /dev/null +++ b/tests/widgets/dyform/export.js @@ -0,0 +1,152 @@ +/** + * Export Models, APIs, Flows, Tables, Tasks, Schedules, etc. + */ + +/** + * Export Apis + * @param {*} dsl + * @returns + */ +function Apis() { + let apis = {}; + apis[`dyform`] = dyformAPI(); + return apis; +} + +/** + * Export Models + * @param {*} dsl + * @returns + */ +function Models(name, dsl) { + let models = {}; + models[`dyform.${name}`] = dyformModel(name); + return models; +} + +/** + * Export Flows + * @param {*} dsl + * @returns + */ +function Flows(name, dsl) { + return [{}]; +} + +/** + * Export Tables + * @param {*} dsl + * @returns + */ +function Tables(name, dsl) { + let tables = {}; + tables[`dyform.${name}`] = dyformTable(name); + return tables; +} + +/** + * Export Tasks + * @param {*} dsl + * @returns + */ +function Tasks(name, dsl) { + let tasks = {}; + tasks[`dyform.${name}`] = dyformTask(name); + return tasks; +} + +/** + * Export Schedules + * @param {*} dsl + * @returns + */ +function Schedules(name, dsl) { + let schedules = {}; + schedules[`dyform.${name}`] = dyformSchedule(name); + return schedules; +} + +function dyformSchedule(name) { + return { + name: `Schedule ${name}`, + schedule: "*/1 * * * *", + process: `widgets.dyform.Save`, + args: [name], + }; +} + +function dyformTask(name) { + return { + name: `Task ${name}`, + worker_nums: 2, + attempts: 3, + attempt_after: 200, + timeout: 2, + size: 1000, + process: `widgets.dyform.Save`, + args: [name], + }; +} + +function dyformAPI() { + return { + name: `dyform API`, + version: "1.0.0", + description: "dyform APIs", + group: "dyform", + guard: "-", + paths: [ + { + path: "/:name/search", + method: "GET", + process: `widgets.dyform.Save`, + in: ["$param.name", ":query"], + out: { status: 200, type: "application/json" }, + }, + ], + }; +} + +function dyformTable(name) { + return { + name: `dyform_${name}`, + version: "1.0.0", + decription: "云服务库", + bind: { model: `dyform.${name}` }, + apis: {}, + columns: {}, + filters: {}, + list: { + primary: "id", + layout: { columns: [{ name: "ID", width: 6 }], filters: [] }, + actions: {}, + }, + edit: { + primary: "id", + layout: { fieldset: [{ columns: [{ name: "ID", width: 6 }] }] }, + actions: { cancel: {} }, + }, + insert: {}, + view: {}, + }; +} + +function dyformModel(name) { + return { + table: { name: `dyform_${name}` }, + columns: [ + { label: "DYFORM ID", name: "id", type: "ID" }, + { label: "SN", name: "sn", type: "string", length: 20, unique: true }, + { label: "NAME", name: "name", type: "string", length: 200, index: true }, + { label: "SOURCE", name: "source", type: "JSON", nullable: true }, + { + label: "TITLE", + name: "title", + type: "string", + length: 200, + index: true, + }, + ], + indexes: [], + }; +} diff --git a/tests/widgets/dyform/helper.js b/tests/widgets/dyform/helper.js new file mode 100644 index 00000000..7c474b22 --- /dev/null +++ b/tests/widgets/dyform/helper.js @@ -0,0 +1,3 @@ +function Foo(param) { + return `${param} World`; +} diff --git a/tests/widgets/dyform/process.js b/tests/widgets/dyform/process.js new file mode 100644 index 00000000..4f41fb2f --- /dev/null +++ b/tests/widgets/dyform/process.js @@ -0,0 +1,24 @@ +/** + * Export the widget processes + * Export() defined witch function in this file will be registered as YAO PROCESS + * The process name is .. + * The processes can be used in compile.js and export.js DIRECTLY + */ + +/** + * SchemaSave + * Save the schema of dyform + * @name: dyform..Save + * @param {String} instance the instance name + * @param {*} payload + */ +function SchemaSave(instance, payload) { + return { instance: instance, payload: payload }; +} + +/** + * Export processes + */ +function Export() { + return { Save: "SchemaSave" }; +} diff --git a/tests/widgets/dyform/widget.json b/tests/widgets/dyform/widget.json new file mode 100644 index 00000000..e19b1256 --- /dev/null +++ b/tests/widgets/dyform/widget.json @@ -0,0 +1,8 @@ +{ + "label": "Dynamic Form", + "description": "A form widget. users can design forms online", + "version": "0.1.0", + "root": "dyforms", + "extension": ".form.json", + "modules": ["Apis", "Models", "Tables", "Tasks", "Schedules"] +} diff --git a/widget/widget.go b/widget/widget.go new file mode 100644 index 00000000..8bd89b0f --- /dev/null +++ b/widget/widget.go @@ -0,0 +1,107 @@ +package widget + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/yaoapp/gou" + "github.com/yaoapp/gou/widget" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/table" +) + +// Load Widgets +func Load(cfg config.Config) error { + var root = filepath.Join(cfg.Root, "widgets") + return LoadFrom(root) +} + +// LoadFrom widget +func LoadFrom(dir string) error { + + register := moduleRegister() + + if share.DirNotExists(dir) { + return fmt.Errorf("%s does not exists", dir) + } + + paths, err := ioutil.ReadDir(dir) + if err != nil { + return err + } + + for _, path := range paths { + + if !path.IsDir() { + continue + } + + name := path.Name() + if _, err := os.Stat(filepath.Join(dir, name, "widget.json")); errors.Is(err, os.ErrNotExist) { + // path/to/whatever does not exist + continue + } + w, err := gou.LoadWidget(filepath.Join(dir, name), name, register) + if err != nil { + return err + } + + // Load instances + err = w.Load() + if err != nil { + return err + } + } + + return err +} + +func moduleRegister() widget.ModuleRegister { + return widget.ModuleRegister{ + "Apis": func(name string, source []byte) error { + _, err := gou.LoadAPIReturn(string(source), name) + log.Trace("[Widget] Register api %s", name) + if err != nil { + log.Error("[Widget] Register api %s %v", name, err) + } + return err + }, + "Models": func(name string, source []byte) error { + _, err := gou.LoadModelReturn(string(source), name) + log.Trace("[Widget] Register model %s", name) + if err != nil { + log.Error("[Widget] Register model %s %v", name, err) + } + return err + }, + "Tables": func(name string, source []byte) error { + log.Trace("[Widget] Register table %s", name) + _, err := table.LoadTable(string(source), name) + if err != nil { + log.Error("[Widget] Register table %s %v", name, err) + } + return nil + }, + "Tasks": func(name string, source []byte) error { + log.Trace("[Widget] Register task %s", name) + _, err := gou.LoadTask(string(source), name) + if err != nil { + log.Error("[Widget] Register task %s %v", name, err) + } + return nil + }, + "Schedules": func(name string, source []byte) error { + log.Trace("[Widget] Register schedule %s", name) + _, err := gou.LoadSchedule(string(source), name) + if err != nil { + log.Error("[Widget] Register schedule %s %v", name, err) + } + return nil + }, + } +} diff --git a/widget/widget_test.go b/widget/widget_test.go new file mode 100644 index 00000000..7183d019 --- /dev/null +++ b/widget/widget_test.go @@ -0,0 +1,25 @@ +package widget + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/share" +) + +func TestLoad(t *testing.T) { + share.DBConnect(config.Conf.DB) // 创建数据库连接 + Load(config.Conf) + LoadFrom("not a path") + v, err := gou.NewProcess("widgets.dyform.Save", "pad", "pay").Exec() + if err != nil { + t.Fatal(err) + } + + res := any.Of(v).Map() + assert.Equal(t, "pad", res.Get("instance")) + assert.Equal(t, "pay", res.Get("payload")) +}