[add] Widget (alpha)

This commit is contained in:
Max 2022-08-05 19:46:26 +08:00
parent 3509956bce
commit 8d88682967
14 changed files with 580 additions and 2 deletions

View file

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

View 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": "订单数量" }]
}
}

View 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": "订单数量" }]
}
}

View file

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

@ -0,0 +1,3 @@
# Widgets
## a customize low-code modules

View file

@ -0,0 +1,3 @@
# DyForm
## A form widget. users can design forms online

View 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.

View 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: "订单数量" }],
},
},
};
}

View 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: [],
};
}

View file

@ -0,0 +1,3 @@
function Foo(param) {
return `${param} World`;
}

View 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" };
}

View 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
View 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
View 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"))
}