[add] Widget (alpha)
This commit is contained in:
parent
3509956bce
commit
8d88682967
14 changed files with 580 additions and 2 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
58
tests/dyforms/app.form.json
Normal file
58
tests/dyforms/app.form.json
Normal file
|
|
@ -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": "订单数量" }]
|
||||
}
|
||||
}
|
||||
77
tests/dyforms/website.form.json
Normal file
77
tests/dyforms/website.form.json
Normal file
|
|
@ -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": "订单数量" }]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 || "-" })}`
|
||||
|
|
|
|||
3
tests/widgets/README.md
Normal file
3
tests/widgets/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Widgets
|
||||
|
||||
## a customize low-code modules
|
||||
3
tests/widgets/dyform/README.md
Normal file
3
tests/widgets/dyform/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# DyForm
|
||||
|
||||
## A form widget. users can design forms online
|
||||
5
tests/widgets/dyform/assets/README.md
Normal file
5
tests/widgets/dyform/assets/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Assets
|
||||
|
||||
1. The assets folder's file will be copied to the `ui/.widgets/<WIDGET NAME>/` folder.
|
||||
2. If the folder does not exist will be created. To upgrade the assets, remove the `ui/.widgets/<WIDGET NAME>/` folder.
|
||||
3. The asset file PUBLIC URL is `http(s)://<YOUR HOST>/.widgets/<WIDGET NAME>/<FILE PATH>`, ANYONE CAN VISIT DIRECTLY.
|
||||
107
tests/widgets/dyform/compile.js
Normal file
107
tests/widgets/dyform/compile.js
Normal file
|
|
@ -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: "订单数量" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
152
tests/widgets/dyform/export.js
Normal file
152
tests/widgets/dyform/export.js
Normal file
|
|
@ -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: [],
|
||||
};
|
||||
}
|
||||
3
tests/widgets/dyform/helper.js
Normal file
3
tests/widgets/dyform/helper.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
function Foo(param) {
|
||||
return `${param} World`;
|
||||
}
|
||||
24
tests/widgets/dyform/process.js
Normal file
24
tests/widgets/dyform/process.js
Normal file
|
|
@ -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 <WIDGET NAME>.<INSTANCE NAME>.<FUNCTION NAME>
|
||||
* The processes can be used in compile.js and export.js DIRECTLY
|
||||
*/
|
||||
|
||||
/**
|
||||
* SchemaSave
|
||||
* Save the schema of dyform
|
||||
* @name: dyform.<INSTANCE>.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" };
|
||||
}
|
||||
8
tests/widgets/dyform/widget.json
Normal file
8
tests/widgets/dyform/widget.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
107
widget/widget.go
Normal file
107
widget/widget.go
Normal file
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
25
widget/widget_test.go
Normal file
25
widget/widget_test.go
Normal file
|
|
@ -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"))
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue