Merge pull request #266 from trheyi/main

[change] the default application
This commit is contained in:
Max 2022-11-17 13:10:22 +08:00 committed by GitHub
commit 1610ba85f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 500 additions and 2459 deletions

View file

@ -79,6 +79,12 @@ jobs:
#ref: 5002c3fded585aaa69a4366135b415ea3234964e
path: xgen-v1.0
- name: Checkout Yao-Init
uses: actions/checkout@v2
with:
repository: yaoapp/yao-init
path: yao-init
- name: Move Kun, Xun, Gou, UI, V8Go
run: |
mv kun ../
@ -87,6 +93,7 @@ jobs:
mv v8go ../
mv xgen-v0.9 ../
mv xgen-v1.0 ../
mv yao-init ../
ls -l .
ls -l ../

View file

@ -74,6 +74,12 @@ jobs:
# ref: 5002c3fded585aaa69a4366135b415ea3234964e
path: xgen-v1.0
- name: Checkout Yao-Init
uses: actions/checkout@v2
with:
repository: yaoapp/yao-init
path: yao-init
- name: Move Kun, Xun, Gou, UI, V8Go
run: |
mv kun ../
@ -82,6 +88,7 @@ jobs:
mv v8go ../
mv xgen-v0.9 ../
mv xgen-v1.0 ../
mv yao-init ../
ls -l .
ls -l ../

View file

@ -139,12 +139,28 @@ pack: bindata fmt
.PHONY: bindata
bindata:
# Setup Workdir
rm -rf .tmp/data
rm -rf .tmp/yao-init
mkdir -p .tmp/data
# Checkout init
git clone https://github.com/YaoApp/yao-init.git .tmp/yao-init
rm -rf .tmp/yao-init/.git
rm -rf .tmp/yao-init/.gitignore
rm -rf .tmp/yao-init/LICENSE
rm -rf .tmp/yao-init/README.md
# Copy Files
cp -r .tmp/yao-init .tmp/data/init
cp -r ui .tmp/data/
cp -r ui .tmp/data/public
cp -r xgen .tmp/data/
cp -r yao .tmp/data/
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
rm -rf .tmp/data
rm -rf .tmp/yao-init
# make artifacts-linux
.PHONY: artifacts-linux
@ -160,15 +176,23 @@ artifacts-linux: clean
rm -f ../xgen-v1.0/pnpm-lock.yaml
echo "BASE=__yao_admin_root" > ../xgen-v1.0/packages/xgen/.env
cd ../xgen-v1.0 && pnpm install && pnpm run build
# Setup UI
cd ../xgen-v1.0/packages/setup && pnpm install && pnpm run build
# Init Application
cd ../yao-init rm -rf .git
cd ../yao-init rm -rf .gitignore
cd ../yao-init rm -rf LICENSE
cd ../yao-init rm -rf README.md
# Packing
mkdir -p .tmp/data/xgen
cp -r ./ui .tmp/data/ui
cp -r ../xgen-v0.9/dist .tmp/data/xgen/v0.9
cp -r ../xgen-v1.0/packages/setup/build .tmp/data/xgen/setup
cp -r ../xgen-v1.0/packages/xgen/dist .tmp/data/xgen/v1.0
cp -r ../yao-init .tmp/data/init
cp -r yao .tmp/data/
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
rm -rf .tmp/data
@ -206,9 +230,15 @@ artifacts-macos: clean
rm -f ../xgen-v1.0/pnpm-lock.yaml
echo "BASE=__yao_admin_root" > ../xgen-v1.0/packages/xgen/.env
cd ../xgen-v1.0 && pnpm install && pnpm run build
# Setup UI
cd ../xgen-v1.0/packages/setup && pnpm install && pnpm run build
# Init Application
cd ../yao-init rm -rf .git
cd ../yao-init rm -rf .gitignore
cd ../yao-init rm -rf LICENSE
cd ../yao-init rm -rf README.md
# Packing
mkdir -p .tmp/data/xgen
@ -216,6 +246,7 @@ artifacts-macos: clean
cp -r ../xgen-v0.9/dist .tmp/data/xgen/v0.9
cp -r ../xgen-v1.0/packages/setup/build .tmp/data/xgen/setup
cp -r ../xgen-v1.0/packages/xgen/dist .tmp/data/xgen/v1.0
cp -r ../yao-init .tmp/data/init
cp -r yao .tmp/data/
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
rm -rf .tmp/data
@ -281,6 +312,14 @@ release: clean
# Setup UI
cd .tmp/xgen/v1.0/packages/setup && pnpm install && pnpm run build
# Checkout init
git clone https://github.com/YaoApp/yao-init.git .tmp/yao-init
rm -rf .tmp/yao-init/.git
rm -rf .tmp/yao-init/.gitignore
rm -rf .tmp/yao-init/LICENSE
rm -rf .tmp/yao-init/README.md
# Packing
mkdir -p .tmp/data/xgen
cp -r ./ui .tmp/data/ui
@ -288,6 +327,7 @@ release: clean
cp -r .tmp/xgen/v0.9/dist .tmp/data/xgen/v0.9
cp -r .tmp/xgen/v1.0/packages/setup/build .tmp/data/xgen/setup
cp -r .tmp/xgen/v1.0/packages/xgen/dist .tmp/data/xgen/v1.0
cp -r .tmp/yao-init .tmp/data/init
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
rm -rf .tmp/data
rm -rf .tmp/xgen
@ -326,6 +366,14 @@ linux-release: clean
# Setup UI
cd .tmp/xgen/v1.0/packages/setup && pnpm install && pnpm run build
# Checkout init
git clone https://github.com/YaoApp/yao-init.git .tmp/yao-init
rm -rf .tmp/yao-init/.git
rm -rf .tmp/yao-init/.gitignore
rm -rf .tmp/yao-init/LICENSE
rm -rf .tmp/yao-init/README.md
# Packing
mkdir -p .tmp/data/xgen
cp -r ./ui .tmp/data/ui
@ -333,6 +381,7 @@ linux-release: clean
cp -r .tmp/xgen/v0.9/dist .tmp/data/xgen/v0.9
cp -r .tmp/xgen/v1.0/packages/setup/build .tmp/data/xgen/setup
cp -r .tmp/xgen/v1.0/packages/xgen/dist .tmp/data/xgen/v1.0
cp -r .tmp/yao-init .tmp/data/init
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
rm -rf .tmp/data
rm -rf .tmp/xgen

View file

@ -43,16 +43,15 @@ func LoadInfo(root string) {
exception.New("解析应用失败 %s", 500, err).Throw()
}
}
if fs.MustExists("/yao/icons/icon.icns") {
if has, _ := fs.Exists("/yao/icons/icon.icns"); has {
info.Icons.Set("icns", xfs.Encode(fs.MustReadFile("/yao/icons/icon.icns")))
}
if fs.MustExists("/yao/icons/icon.ico") {
if has, _ := fs.Exists("/yao/icons/icon.ico"); has {
info.Icons.Set("ico", xfs.Encode(fs.MustReadFile("/yao/icons/icon.ico")))
}
if fs.MustExists("/yao/icons/icon.png") {
if has, _ := fs.Exists("/yao/icons/icon.png"); has {
info.Icons.Set("png", xfs.Encode(fs.MustReadFile("/yao/icons/icon.png")))
}

File diff suppressed because one or more lines are too long

View file

@ -7,12 +7,9 @@ import (
"strings"
"github.com/yaoapp/gou"
"github.com/yaoapp/gou/fs"
"github.com/yaoapp/gou/fs/dsl"
"github.com/yaoapp/gou/fs/system"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/data"
"github.com/yaoapp/yao/engine"
"github.com/yaoapp/yao/share"
"github.com/yaoapp/yao/widgets/app"
)
@ -95,7 +92,7 @@ func Install(payload map[string]map[string]string) error {
return err
}
err = makeDemoWidgets(root)
err = makeInit(root)
if err != nil {
return err
}
@ -231,897 +228,26 @@ func makeDirs(root string) error {
return nil
}
func makeDemoWidgets(root string) error {
dsl := dsl.New(root)
// make app
ok, err := makeAppWidget(dsl, root)
if err != nil || !ok {
return err
}
// make flows
err = makeMenuFlowWidget(dsl, root)
func makeInit(root string) error {
files := data.AssetNames()
for _, file := range files {
if strings.HasPrefix(file, "init/") {
dst := filepath.Join(root, strings.TrimPrefix(file, "init/"))
content, err := data.Read(file)
if err != nil {
return err
}
// make logins
err = makeLoginWidget(dsl, root)
if err != nil {
dir := filepath.Dir(dst)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
// make models
err = makeModelWidget(dsl, root)
if err != nil {
if err = os.WriteFile(dst, content, 0644); err != nil {
return err
}
// make tables & forms
err = makeTableFormWidget(dsl, root)
if err != nil {
return err
}
// make chart
err = makeChartWidget(dsl, root)
if err != nil {
return err
}
// make scripts
err = makeScript(root)
if err != nil {
return err
}
// make scripts
err = makeLang(root)
if err != nil {
return err
}
// make index page
err = makeIndexPage(root)
if err != nil {
return err
}
return nil
}
func makeAppWidget(dsl fs.FileSystem, root string) (bool, error) {
file := filepath.Join("app.json")
if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists
return false, nil
}
_, err := dsl.WriteFile(file, []byte(fmt.Sprintf(`
{
"xgen": "1.0",
"name": "::Demo Application",
"short": "::Demo",
"description": "::Another yao application",
"version": "%s",
"adminRoot": "admin",
"setup": "scripts.demo.Data",
"menu": {
"process": "flows.app.menu",
"args": ["demo"]
},
"optional": {
"hideNotification": true,
"hideSetting": false
}
}
`, share.VERSION)), 0644)
if err != nil {
return false, err
}
return true, nil
}
func makeMenuFlowWidget(dsl fs.FileSystem, root string) error {
file := filepath.Join("flows", "app", "menu.flow.json")
if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists
return nil
}
_, err := dsl.WriteFile(file, []byte(`
{
"name": "APP Menu",
"nodes": [],
"output": [
{
"blocks": 0,
"icon": "icon-activity",
"id": 1,
"name": "图表",
"parent": null,
"path": "/x/Chart/dashboard",
"visible_menu": 0
},
{
"blocks": 0,
"icon": "icon-book",
"id": 2,
"name": "表格",
"parent": null,
"path": "/x/Table/pet",
"visible_menu": 1,
"children": [
{
"blocks": 0,
"icon": "icon-book",
"name": "宠物列表",
"id": 2010,
"parent": 2,
"path": "/x/Table/pet",
"visible_menu": 1
}
]
},
{
"blocks": 0,
"icon": "icon-clipboard",
"id": 2,
"name": "表单",
"parent": null,
"path": "/x/Form/pet/1/edit",
"visible_menu": 1,
"children": [
{
"blocks": 0,
"icon": "icon-clipboard",
"name": "编辑模式",
"id": 2010,
"parent": 2,
"path": "/x/Form/pet/1/edit",
"visible_menu": 1
},
{
"blocks": 0,
"icon": "icon-clipboard",
"name": "查看模式",
"id": 2010,
"parent": 2,
"path": "/x/Form/pet/1/view",
"visible_menu": 1
}
]
}
]
}
`), 0644)
if err != nil {
return err
}
return nil
}
func makeLoginWidget(dsl fs.FileSystem, root string) error {
file := filepath.Join("logins", "admin.login.json")
if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists
return nil
}
_, err := dsl.WriteFile(file, []byte(`
{
"name": "::Admin Login",
"action": {
"process": "yao.login.Admin",
"args": [":payload"]
},
"layout": {
"entry": "/x/Chart/dashboard",
"slogan": "::Make Your Dream With Yao App Engine",
"site": "https://yaoapps.com?from=instance-admin-login"
}
}
`), 0644)
if err != nil {
return err
}
file = filepath.Join("logins", "user.login.json")
if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists
return nil
}
_, err = dsl.WriteFile(file, []byte(`
{
"name": "::User Login",
"action": {
"process": "scripts.user.Login",
"args": [":payload"]
},
"layout": {
"entry": "/x/Table/pet",
"slogan": "::Make Your Dream With Yao App Engine",
"site": "https://yaoapps.com/from=instance-user-login"
}
}
`), 0644)
if err != nil {
return err
}
return nil
}
func makeModelWidget(dsl fs.FileSystem, root string) error {
file := filepath.Join("models", "pet.mod.json")
if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists
return nil
}
_, err := dsl.WriteFile(file, []byte(`
{
"name": "Pet",
"table": { "name": "pet", "comment": "宠物表" },
"columns": [
{ "name": "id", "comment": "ID", "type": "ID" },
{
"name": "name",
"comment": "昵称",
"type": "string",
"length": 80,
"index": true,
"nullable": true
},
{
"name": "type",
"comment": "类型",
"type": "enum",
"option": ["cat", "dog", "others"],
"index": true
},
{
"name": "status",
"comment": "入院状态",
"type": "enum",
"option": ["checked", "curing", "cured"],
"index": true
},
{
"name": "mode",
"comment": "状态",
"type": "enum",
"option": ["enabled", "disabled"],
"index": true
},
{
"name": "online",
"comment": "是否在线",
"type": "boolean",
"default": false,
"index": true
},
{
"name": "curing_status",
"comment": "治疗状态",
"type": "enum",
"default": "0",
"option": ["0", "1", "2"],
"index": true
},
{
"name": "stay",
"comment": "入院时间",
"type": "integer"
},
{
"name": "cost",
"comment": "花费",
"type": "integer"
},
{
"name": "doctor_id",
"type": "integer",
"comment": "相关职工",
"nullable": true
},
{
"name": "images",
"type": "json",
"comment": "相关图片",
"nullable": true
},
{
"name": "test_string",
"comment": "测试字段(字符串)",
"type": "string",
"nullable": true
},
{
"name": "test_number",
"comment": "测试字段(数字)",
"type": "integer",
"nullable": true
},
{
"name": "test_array",
"comment": "测试字段(数组)",
"type": "json",
"nullable": true
}
],
"relations": {},
"values": [],
"indexes": [],
"option": { "timestamps": true,"soft_deletes": true }
}
`), 0644)
if err != nil {
return err
}
return nil
}
func makeTableFormWidget(dsl fs.FileSystem, root string) error {
file := filepath.Join("tables", "pet.tab.json")
if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists
return nil
}
_, err := dsl.WriteFile(file, []byte(`
{
"name": "::Pet Admin Bind Model",
"action": {
"bind": { "model": "pet", "option": {} }
}
}
`), 0644)
if err != nil {
return err
}
file = filepath.Join("forms", "pet.form.json")
if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists
return nil
}
_, err = dsl.WriteFile(file, []byte(`
{
"name": "::Pet Admin Bind Table",
"action": {
"bind": { "table": "pet", "option": {} }
}
}
`), 0644)
if err != nil {
return err
}
return nil
}
func makeChartWidget(dsl fs.FileSystem, root string) error {
file := filepath.Join("charts", "dashboard.chart.json")
if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists
return nil
}
_, err := dsl.WriteFile(file, []byte(`
{
"name": "宠物医院数据图表",
"config":{"full":false},
"action": {
"before:data": "scripts.stat.BeforeData",
"data": { "process": "scripts.stat.Data", "default": ["2022-09-20"] },
"after:data": "scripts.stat.AfterData"
},
"layout": {
"operation": {
"actions": [
{
"title": "跳转至大屏",
"icon": "icon-airplay",
"action": { "Common.historyPush": { "pathname": "/x/Cool/demo" } }
}
]
},
"filter": {
"columns": [{ "name": "时间区间", "width": 6 }]
},
"chart": {
"columns": [
{ "name": "宠物数量", "width": 6 },
{ "name": "宠物类型", "width": 6 },
{ "name": "当月收入", "width": 6 },
{ "name": "医师数量", "width": 6 },
{ "name": "宠物数量_上月", "width": 6 },
{ "name": "宠物类型_上月", "width": 6 },
{ "name": "当月收入_上月", "width": 6 },
{ "name": "医师数量_上月", "width": 6 },
{ "name": "收入", "width": 8 },
{ "name": "支出", "width": 8 },
{ "name": "综合评分", "width": 8 },
{ "name": "收入_折线图", "width": 8 },
{ "name": "支出_折线图", "width": 8 },
{ "name": "综合评分_折线图", "width": 8 },
{ "name": "类型排布", "width": 12 },
{ "name": "状态分布", "width": 12 },
{ "name": "综合消费", "width": 24 }
]
}
},
"fields": {
"filter": {
"时间区间": {
"bind": "range",
"edit": { "type": "RangePicker", "props": {} }
},
"状态": {
"bind": "status",
"edit": {
"type": "Select",
"props": {
"xProps": {
"$remote": {
"process": "models.pet.Get",
"query": { "select": "name,status", "limit": 2 }
}
}
}
}
}
},
"chart": {
"收入": {
"bind": "income",
"link": "/x/Table/pet",
"out": "scripts.stat.Income",
"view": {
"type": "NumberChart",
"props": {
"chartHeight": 150,
"prefix": "¥",
"decimals": 2,
"nameKey": "date",
"valueKey": "value"
}
}
},
"支出": {
"bind": "cost",
"link": "/x/Table/pet",
"view": {
"type": "NumberChart",
"props": {
"chartHeight": 150,
"color": "red",
"prefix": "¥",
"decimals": 2,
"nameKey": "date",
"valueKey": "value"
}
}
},
"综合评分": {
"bind": "rate",
"link": "/x/Table/pet",
"view": {
"type": "NumberChart",
"props": {
"chartHeight": 150,
"color": "orange",
"unit": "分",
"decimals": 1,
"nameKey": "date",
"valueKey": "value"
}
}
},
"收入_折线图": {
"bind": "income",
"link": "/x/Table/pet",
"view": {
"type": "NumberChart",
"props": {
"cardStyle": { "padding": 0 },
"type": "line",
"chartHeight": 120,
"prefix": "¥",
"decimals": 2,
"nameKey": "date",
"valueKey": "value"
}
}
},
"支出_折线图": {
"bind": "cost",
"link": "/x/Table/pet",
"view": {
"type": "NumberChart",
"props": {
"cardStyle": { "padding": 0 },
"type": "line",
"chartHeight": 120,
"color": "red",
"prefix": "¥",
"decimals": 2,
"nameKey": "date",
"valueKey": "value"
}
}
},
"综合评分_折线图": {
"bind": "rate",
"link": "/x/Table/pet",
"view": {
"type": "NumberChart",
"props": {
"cardStyle": { "padding": 0 },
"type": "line",
"chartHeight": 120,
"color": "orange",
"unit": "分",
"decimals": 1,
"nameKey": "date",
"valueKey": "value"
}
}
},
"宠物数量": {
"bind": "pet_count",
"link": "/x/Table/pet",
"view": { "type": "Number", "props": { "unit": "个" } }
},
"宠物类型": {
"bind": "pet_type",
"view": { "type": "Number", "props": { "unit": "种" } }
},
"当月收入": {
"bind": "income_monthly",
"view": { "type": "Number", "props": { "unit": "元" } }
},
"医师数量": {
"bind": "doctor_count",
"view": { "type": "Number", "props": { "unit": "个" } }
},
"宠物数量_上月": {
"bind": "prev_pet_count",
"link": "/x/Table/pet",
"view": {
"type": "Number",
"props": {
"cardStyle": { "padding": 0 },
"unit": "个",
"prev_title": "上月数据"
}
}
},
"宠物类型_上月": {
"bind": "prev_pet_type",
"link": "/x/Table/pet",
"view": {
"type": "Number",
"props": {
"cardStyle": { "padding": 0 },
"unit": "种",
"prev_title": "上月数据"
}
}
},
"当月收入_上月": {
"bind": "prev_income_monthly",
"link": "/x/Table/pet",
"view": {
"type": "Number",
"props": {
"cardStyle": { "padding": 0 },
"unit": "元",
"prev_title": "上月数据"
}
}
},
"医师数量_上月": {
"bind": "prev_doctor_count",
"link": "/x/Table/pet",
"view": {
"type": "Number",
"props": {
"cardStyle": { "padding": 0 },
"unit": "个",
"prev_title": "上月数据"
}
}
},
"类型排布": {
"bind": "datasource_type",
"link": "/x/Table/pet",
"view": {
"type": "Pie",
"props": {
"height": 240,
"nameKey": "type",
"series": [
{
"valueKey": "count",
"roseType": "area",
"radius": [10, 100],
"center": ["60%", "50%"],
"itemStyle": { "borderRadius": 6 }
}
]
}
},
"refer": {
"type": "Table",
"props": {
"columns": [
{ "title": "类型", "dataIndex": "type" },
{ "title": "数量", "dataIndex": "count" }
]
}
}
},
"状态分布": {
"bind": "datasource_status",
"view": {
"type": "Bar",
"props": {
"height": 240,
"nameKey": "status",
"axisLabel": { "interval": 0, "fontSize": 12 },
"series": [
{
"valueKey": "count",
"type": "bar",
"colorBy": "data",
"itemStyle": { "borderRadius": 6 },
"splitLine": { "show": false },
"axisLabel": { "show": false }
}
]
}
},
"refer": {
"type": "Table",
"props": {
"columns": [
{ "title": "状态", "dataIndex": "status" },
{ "title": "数量", "dataIndex": "count" }
]
}
}
},
"综合消费": {
"bind": "datasource_cost",
"view": {
"type": "LineBar",
"props": {
"height": 240,
"nameKey": "name",
"axisLabel": { "interval": 0, "fontSize": 12 },
"series": [
{
"valueKey": "stay",
"type": "line",
"smooth": true,
"symbolSize": 8,
"itemStyle": { "borderRadius": 6 },
"splitLine": { "show": false },
"axisLabel": { "show": false }
},
{
"valueKey": "cost",
"type": "bar",
"itemStyle": { "borderRadius": 6 },
"splitLine": { "show": false },
"axisLabel": { "show": false }
}
]
}
}
}
}
}
}
`), 0644)
if err != nil {
return err
}
return nil
}
func makeScript(root string) error {
fs := system.New(filepath.Join(root, "scripts"))
file := "demo.js"
if _, err := os.Stat(filepath.Join(root, "scripts", file)); err == nil { // exists
return nil
}
_, err := fs.WriteFile(file, []byte(`
/**
* Demo Data
*/
function Data() {
return Process(
"yao.table.Insert",
"pet",
["name", "type", "status", "mode", "stay", "cost", "doctor_id"],
[
["Cookie", "cat", "checked", "enabled", 200, 105, 1],
["Baby", "dog", "checked", "enabled", 186, 24, 1],
["Poo", "others", "checked", "enabled", 199, 66, 1],
]
);
}
`), 0644)
if err != nil {
return err
}
file = filepath.Join("stat.js")
if _, err := os.Stat(filepath.Join(root, "scripts", file)); err == nil { // exists
return nil
}
_, err = fs.WriteFile(file, []byte(`
/**
* before:data hook
* @param {*} params
* @returns
*/
function BeforeData(params) {
log.Info("[chart] before data hook: %s", JSON.stringify(params));
return [params];
}
/**
* after:data hook
* @param {*} data
* @returns
*/
function AfterData(data) {
log.Info("[chart] after data hook: %s", JSON.stringify(data));
return data;
}
/**
* Get Data
* @param {*} params
*/
function Data(params) {
log.Info("[chart] process data query: %s", JSON.stringify(params));
return {
income: [
{ value: 40300, date: "2022-1-1" },
{ value: 50800, date: "2022-2-1" },
{ value: 31300, date: "2022-3-1" },
{ value: 48800, date: "2022-4-1" },
{ value: 69900, date: "2022-5-1" },
{ value: 37800, date: "2022-6-1" },
],
cost: [
{ value: 28100, date: "2022-1-1" },
{ value: 23000, date: "2022-2-1" },
{ value: 29300, date: "2022-3-1" },
{ value: 26700, date: "2022-4-1" },
{ value: 26400, date: "2022-5-1" },
{ value: 31200, date: "2022-6-1" },
],
rate: [
{ value: 8.0, date: "2022-1-1" },
{ value: 7.6, date: "2022-2-1" },
{ value: 9.1, date: "2022-3-1" },
{ value: 8.4, date: "2022-4-1" },
{ value: 6.9, date: "2022-5-1" },
{ value: 9.0, date: "2022-6-1" },
],
pet_count: 54,
pet_type: 8,
income_monthly: 68900,
doctor_count: 23,
prev_pet_count: { current: 54, prev: 45 },
prev_pet_type: { current: 8, prev: 13 },
prev_income_monthly: { current: 68900, prev: 92000 },
prev_doctor_count: { current: 23, prev: 27 },
datasource_type: [
{ type: "猫猫", count: 18 },
{ type: "狗狗", count: 6 },
{ type: "其他", count: 3 },
],
datasource_status: [
{ status: "已查看", count: 3 },
{ status: "治疗中", count: 12 },
{ status: "已治愈", count: 9 },
],
datasource_cost: [
{ name: "毛毛", stay: 3, cost: 2000 },
{ name: "阿布", stay: 6, cost: 4200 },
{ name: "咪咪", stay: 7, cost: 6000 },
{ name: "狗蛋", stay: 1, cost: 1000 },
],
};
}
/**
* Compute out
* @param {*} field
* @param {*} value
* @param {*} data
* @returns
*/
function Income(field, value, data) {
log.Info(
"[chart] Income Compute: %s",
JSON.stringify({ field: field, value: value, data: data })
);
return value;
}
`), 0644)
if err != nil {
return err
}
return nil
}
func makeLang(root string) error {
fs := system.New(filepath.Join(root, "langs"))
file := filepath.Join("zh-cn", "global.yml")
if _, err := os.Stat(filepath.Join(root, "langs", file)); err == nil { // exists
return nil
}
_, err := fs.WriteFile(file, []byte(`
Demo: 演示
Demo Application: 示例应用
Another yao application: 又一个 YAO 应用`), 0644)
if err != nil {
return err
}
file = filepath.Join("zh-hk", "langs", "global.yml")
if _, err := os.Stat(filepath.Join(root, file)); err == nil { // exists
return nil
}
_, err = fs.WriteFile(file, []byte(`
Demo: 演示
Demo Application: 示例應用
Another yao application: 又一個YAO應用`), 0644)
if err != nil {
return err
}
return nil
}
func makeIndexPage(root string) error {
fs := system.New(filepath.Join(root, "public"))
file := "index.html"
if _, err := os.Stat(filepath.Join(root, "public", file)); err == nil { // exists
return nil
}
_, err := fs.WriteFile(file, []byte(`
It works!`), 0644)
if err != nil {
return err
}
return nil
}

View file

@ -1,6 +1,7 @@
package setup
import (
"os"
"testing"
)
@ -10,3 +11,19 @@ func TestInstall(t *testing.T) {
// t.Fatal(err)
// }
}
func TestMakeInit(t *testing.T) {
root := prepare(t)
err := makeInit(root)
if err != nil {
t.Fatal(err)
}
}
func prepare(t *testing.T) string {
dir, err := os.MkdirTemp("", "-install")
if err != nil {
t.Fatal(err)
}
return dir
}

View file

@ -47,6 +47,9 @@ func (layout *LayoutDSL) BindModel(m *gou.Model, formID string, fields *FieldsDS
if ok && name != "deleted_at" {
if col, has := fields.formMap[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") {
@ -137,6 +140,9 @@ func (layout *LayoutDSL) BindTable(tab *table.DSL, formID string, fields *Fields
name := column.Key
if col, has := fields.Form[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}})
}
}

View file

@ -1,107 +0,0 @@
package workflow
import (
"path/filepath"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou"
)
// API:
// 读取工作流 GET /api/xiang/workflow/<工作流名称>/find/:id
// 读取工作流 GET /api/xiang/workflow/<工作流名称>/open/:data_id
// 读取工作流配置 GET /api/xiang/workflow/<工作流名称>/setting/:data_id
// 调用自定义API POST /api/xiang/workflow/<工作流名称>/<自定义API路由>
// SetupAPIs 注册API
func (workflow *WorkFlow) SetupAPIs() error {
api := &gou.HTTP{
Name: "工作流接口",
Version: "1.0.0",
Description: "工作流接口 API",
Group: "xiang/workflow",
Guard: "bearer-jwt",
Paths: []gou.Path{},
}
workflow.AddAPI(api, workflow.apiFind())
workflow.AddAPI(api, workflow.apiOpen())
workflow.AddAPI(api, workflow.apiSetting())
for name, custAPI := range workflow.APIs {
workflow.AddAPI(api, gou.Path{
Label: custAPI.Label,
Description: custAPI.Description,
Path: filepath.Join("/", workflow.Name, name),
Method: "POST",
Process: custAPI.Process,
In: []string{workflow.Name, "$session.user_id", "$query.data_id", ":payload"},
Out: gou.Out{
Status: 200,
Type: "application/json",
},
})
}
// 注册API
source, err := jsoniter.Marshal(api)
if err != nil {
return err
}
gou.LoadAPI(string(source), "xiang.workflow."+workflow.Name)
return nil
}
// AddAPI 添加API
func (workflow *WorkFlow) AddAPI(api *gou.HTTP, path gou.Path) {
api.Paths = append(api.Paths, path)
}
// apiFind 使用ID读取工作流
func (workflow *WorkFlow) apiFind() gou.Path {
return gou.Path{
Label: "读取工作流",
Description: "使用工作流ID读取工作流",
Path: filepath.Join("/", workflow.Name, "find", ":id"),
Method: "GET",
Process: "xiang.workflow.Find",
In: []string{workflow.Name, "$param.id"},
Out: gou.Out{
Status: 200,
Type: "application/json",
},
}
}
// apiOpen 使用用户ID和数据ID 读取工作流
func (workflow *WorkFlow) apiOpen() gou.Path {
return gou.Path{
Label: "读取工作流",
Description: "使用数据ID读取工作流",
Path: filepath.Join("/", workflow.Name, "open", ":data_id"),
Method: "GET",
Process: "xiang.workflow.Open",
In: []string{workflow.Name, "$session.user_id", "$param.data_id"},
Out: gou.Out{
Status: 200,
Type: "application/json",
},
}
}
// apiSetting 读取工作流配置
func (workflow *WorkFlow) apiSetting() gou.Path {
return gou.Path{
Label: "读取工作流配置",
Description: "使用数据ID读取工作流配置",
Path: filepath.Join("/", workflow.Name, "setting", ":data_id"),
Method: "GET",
Process: "xiang.workflow.Setting",
In: []string{workflow.Name, "$session.user_id", "$param.data_id"},
Out: gou.Out{
Status: 200,
Type: "application/json",
},
}
}

View file

@ -1,12 +0,0 @@
package workflow
// Workflow will be removed from engine
// func TestSetupAPIs(t *testing.T) {
// assignFlow := Select("assign")
// assignFlow.SetupAPIs()
// api := gou.SelectAPI("xiang.workflow.assign")
// assert.Equal(t, "xiang.workflow.assign", api.Name)
// assert.Equal(t, 10, len(api.HTTP.Paths))
// }

View file

@ -1,151 +0,0 @@
package workflow
import "github.com/yaoapp/gou"
// WARNING: the Workflow widget will be removed from yao engine
// Process
// 读取工作流 xiang.workflow.Find(name, workflow_id)
// 读取工作流 xiang.workflow.Open(name, uid, data_id)
// 保存工作流 xiang.workflow.Save(name, uid, node_name, data_id, input, ...output)
// 进入下一个节点 xiang.workflow.Next(name, uid, workflow_id, output)
// 跳转到指定节点 xiang.workflow.Goto(name, uid, workflow_id, node_name, output)
// 更新工作流状态 xiang.workflow.Status(name, uid, workflow_id, status_name, output)
// 标记结束流程 xiang.workflow.Done(name, uid, workflow_id, output)
// 标记关闭流程 xiang.workflow.Close(name, uid, workflow_id, output)
// 标记重置流程 xiang.workflow.Reset(name, uid, workflow_id, output)
func init() {
// 注册处理器
gou.RegisterProcessHandler("xiang.workflow.Find", ProcessFind)
gou.RegisterProcessHandler("xiang.workflow.Setting", ProcessSetting)
gou.RegisterProcessHandler("xiang.workflow.Open", ProcessOpen)
gou.RegisterProcessHandler("xiang.workflow.Save", ProcessSave)
gou.RegisterProcessHandler("xiang.workflow.Next", ProcessNext)
gou.RegisterProcessHandler("xiang.workflow.Goto", ProcessGoto)
gou.RegisterProcessHandler("xiang.workflow.Status", ProcessStatus)
gou.RegisterProcessHandler("xiang.workflow.Done", ProcessDone)
gou.RegisterProcessHandler("xiang.workflow.Close", ProcessClose)
gou.RegisterProcessHandler("xiang.workflow.Reset", ProcessReset)
}
// ProcessFind xiang.workflow.Find 读取工作流
//
// args: [工作流名称*, 工作流ID*]
//
// return: map[string]interface{} 工作流数据记录
func ProcessFind(process *gou.Process) interface{} {
process.ValidateArgNums(2)
wflow := Select(process.ArgsString(0))
return wflow.Find(process.ArgsInt(1))
}
// ProcessOpen xiang.workflow.Open 读取工作流
//
// args: [工作流名称*, 当前用户ID*, 关联数据ID*]
//
// return: map[string]interface{} 工作流数据记录
func ProcessOpen(process *gou.Process) interface{} {
process.ValidateArgNums(3)
wflow := Select(process.ArgsString(0))
return wflow.Open(process.ArgsInt(1), process.Args[2])
}
// ProcessSetting xiang.workflow.Setting 读取工作流配置
//
// args: [工作流名称*, 当前用户ID*, 关联数据ID*]
//
// return: map[string]interface{} 工作流配置
func ProcessSetting(process *gou.Process) interface{} {
process.ValidateArgNums(3)
wflow := Select(process.ArgsString(0))
return wflow.Setting(process.ArgsInt(1), process.Args[2])
}
// ProcessSave xiang.workflow.Save 保存工作流节点信息
//
// args: [工作流名称*, 当前用户ID*, 节点名称*, 关联数据ID*, 输入数据*, 输出数据] (输入数据: {"data":{}, "form":{}} data 关联数据记录信息, form 工作流body表单信息, 输出数据: {"foo":"bar"} )
//
// return: map[string]interface{} 工作流数据记录
func ProcessSave(process *gou.Process) interface{} {
process.ValidateArgNums(5)
wflow := Select(process.ArgsString(0))
input := InputOf(process.ArgsMap(4))
output := map[string]interface{}{}
if process.NumOfArgsIs(6) {
output = process.ArgsMap(5)
}
return wflow.Save(process.ArgsInt(1), process.ArgsString(2), process.Args[3], input, output)
}
// ProcessNext xiang.workflow.Next 进入下一个节点
//
// args: [工作流名称*, 当前用户ID*, 工作流ID*, 输出数据*] (输出数据: {"foo":"bar"} )
//
// return: map[string]interface{} 工作流数据记录
func ProcessNext(process *gou.Process) interface{} {
process.ValidateArgNums(4)
wflow := Select(process.ArgsString(0))
output := process.ArgsMap(3)
return wflow.Next(process.ArgsInt(1), process.ArgsInt(2), output)
}
// ProcessGoto xiang.workflow.Goto 跳转到指定节点
//
// args: [工作流名称*, 当前用户ID*, 工作流ID*, 节点名称*, 输出数据*] (输出数据: {"foo":"bar"} )
//
// return: map[string]interface{} 工作流数据记录
func ProcessGoto(process *gou.Process) interface{} {
process.ValidateArgNums(5)
wflow := Select(process.ArgsString(0))
output := process.ArgsMap(4)
return wflow.Goto(process.ArgsInt(1), process.ArgsInt(2), process.ArgsString(3), output)
}
// ProcessStatus xiang.workflow.Status 更新工作流状态
//
// args: [工作流名称*, 当前用户ID*, 工作流ID*, 状态名称*, 输出数据*] (输出数据: {"foo":"bar"} )
//
// return: map[string]interface{} 工作流数据记录
func ProcessStatus(process *gou.Process) interface{} {
process.ValidateArgNums(5)
wflow := Select(process.ArgsString(0))
output := process.ArgsMap(4)
return wflow.Status(process.ArgsInt(1), process.ArgsInt(2), process.ArgsString(3), output)
}
// ProcessDone xiang.workflow.Done 标记结束流程
//
// args: [工作流名称*, 当前用户ID*, 工作流ID*, 输出数据*] (输出数据: {"foo":"bar"} )
//
// return: map[string]interface{} 工作流数据记录
func ProcessDone(process *gou.Process) interface{} {
process.ValidateArgNums(4)
wflow := Select(process.ArgsString(0))
output := process.ArgsMap(3)
return wflow.Done(process.ArgsInt(1), process.ArgsInt(2), output)
}
// ProcessClose xiang.workflow.Close 标记关闭流程
//
// args: [工作流名称*, 当前用户ID*, 工作流ID*, 输出数据*] (输出数据: {"foo":"bar"} )
//
// return: map[string]interface{} 工作流数据记录
func ProcessClose(process *gou.Process) interface{} {
process.ValidateArgNums(4)
wflow := Select(process.ArgsString(0))
output := process.ArgsMap(3)
return wflow.Close(process.ArgsInt(1), process.ArgsInt(2), output)
}
// ProcessReset xiang.workflow.Reset 标记重置流程
//
// args: [工作流名称*, 当前用户ID*, 工作流ID*, 输出数据*] (输出数据: {"foo":"bar"} )
//
// return: map[string]interface{} 工作流数据记录
func ProcessReset(process *gou.Process) interface{} {
process.ValidateArgNums(4)
wflow := Select(process.ArgsString(0))
output := process.ArgsMap(3)
return wflow.Reset(process.ArgsInt(1), process.ArgsInt(2), output)
}

View file

@ -1,233 +0,0 @@
package workflow
// WARNING: the Workflow widget will be removed from yao engine
// func init() {
// share.DBConnect(config.Conf.DB)
// share.Load(config.Conf)
// model.Load(config.Conf)
// engineModels := path.Join(os.Getenv("YAO_DEV"), "yao", "models")
// model.LoadFrom(engineModels, "xiang.")
// query.Load(config.Conf)
// flow.Load(config.Conf)
// Load(config.Conf)
// }
// func TestProcessFind(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// args := []interface{}{"assign", wflow["id"]}
// res := gou.NewProcess("xiang.workflow.Find", args...).Run()
// data := any.Of(res).Map().MapStrAny.Dot()
// assert.Equal(t, wflow["id"], data.Get("id"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestProcessSetting(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// args := []interface{}{"assign", 1, 1}
// res := gou.NewProcess("xiang.workflow.Setting", args...).Run()
// data := any.Of(res).Map().MapStrAny.Dot()
// assert.Equal(t, 2, data.Get("nodes.2.source"))
// assert.Equal(t, 2, data.Get("nodes.3.source"))
// assert.Equal(t, "assign", data.Get("name"))
// assert.Equal(t, "指派商务负责人", data.Get("label"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestProcessOpen(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// args := []interface{}{"assign", 1, 1}
// res := gou.NewProcess("xiang.workflow.Open", args...).Run()
// data := any.Of(res).Map().MapStrAny.Dot()
// assert.Equal(t, wflow["id"], data.Get("id"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestProcessSave(t *testing.T) {
// args := []interface{}{"assign",
// 1, "选择商务负责人", 1,
// map[string]interface{}{
// "data": map[string]interface{}{"id": 1, "name": "云主机"},
// "form": map[string]interface{}{"biz_id": 1, "name": "张良明"},
// },
// map[string]interface{}{"foo": "bar"},
// }
// res := gou.NewProcess("xiang.workflow.Save", args...).Run()
// data := any.Of(res).Map().MapStrAny.Dot()
// assert.Equal(t, int64(1), data.Get("id"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestProcessNext(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// args := []interface{}{"assign",
// 1, wflow["id"], map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// },
// }
// res := gou.NewProcess("xiang.workflow.Next", args...).Run()
// data := any.Of(res).Map().MapStrAny.Dot()
// assert.Equal(t, wflow["id"], data.Get("id"))
// assert.Equal(t, "项目负责人审批", data.Get("node_name"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestProcessGoto(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// args := []interface{}{"assign",
// 2, id, "选择商务负责人",
// map[string]interface{}{"审批结果": "驳回"},
// }
// res := gou.NewProcess("xiang.workflow.Goto", args...).Run()
// data := any.Of(res).Map().MapStrAny.Dot()
// assert.Equal(t, wflow["id"], data.Get("id"))
// assert.Equal(t, "选择商务负责人", data.Get("node_name"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestProcessStatus(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// args := []interface{}{"assign",
// 2, id, "已完成", map[string]interface{}{"关闭原因": "测试完成"},
// }
// res := gou.NewProcess("xiang.workflow.Status", args...).Run()
// data := any.Of(res).Map().MapStrAny.Dot()
// assert.Equal(t, wflow["id"], data.Get("id"))
// assert.Equal(t, "已完成", data.Get("status"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestProcessDone(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// args := []interface{}{"assign",
// 2, id, map[string]interface{}{"关闭原因": "测试完成"},
// }
// res := gou.NewProcess("xiang.workflow.Done", args...).Run()
// data := any.Of(res).Map().MapStrAny.Dot()
// assert.Equal(t, wflow["id"], data.Get("id"))
// assert.Equal(t, "已完成", data.Get("status"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestProcessClose(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// args := []interface{}{"assign",
// 2, id, map[string]interface{}{"关闭原因": "测试关闭"},
// }
// res := gou.NewProcess("xiang.workflow.Close", args...).Run()
// data := any.Of(res).Map().MapStrAny.Dot()
// assert.Equal(t, wflow["id"], data.Get("id"))
// assert.Equal(t, "已关闭", data.Get("status"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestProcessReset(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// args := []interface{}{"assign",
// 2, id, map[string]interface{}{"驳回原因": "字段填写错误"},
// }
// res := gou.NewProcess("xiang.workflow.Reset", args...).Run()
// data := any.Of(res).Map().MapStrAny.Dot()
// assert.Equal(t, wflow["id"], data.Get("id"))
// assert.Equal(t, "进行中", data.Get("status"))
// assert.Equal(t, "选择商务负责人", data.Get("node_name"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }

View file

@ -1,53 +0,0 @@
package workflow
import (
"github.com/yaoapp/yao/helper"
"github.com/yaoapp/yao/share"
)
// WorkFlow 工作流配置结构
type WorkFlow struct {
Name string `json:"-"`
Source []byte `json:"-"`
Version string `json:"version"`
Label string `json:"label,omitempty"`
Decription string `json:"decription,omitempty"`
Nodes []Node `json:"nodes"`
APIs map[string]API `json:"apis"`
Actions map[string]share.Render `json:"actions"`
}
// Node 工作流节点
type Node struct {
Name string `json:"name"`
Body share.Render `json:"body,omitempty"`
Actions []string `json:"actions,omitempty"`
User User `json:"user,omitempty"`
Next []Next `json:"next,omitempty"`
}
// Next 下一个节点描述
type Next struct {
Conditions []helper.Condition `json:"when,omitempty"`
Goto string `json:"goto,omitempty"`
}
// User 工作流相关用户读取条件
type User struct {
Process string `json:"process"`
Args []interface{} `json:"args"`
}
// API 工作相关API
type API struct {
Label string `json:"label,omitempty"`
Description string `json:"description,omitempty"`
Process string `json:"process"`
Args []interface{} `json:"args"`
}
// Input 用户输入数据
type Input struct {
Data map[string]interface{} `json:"data"` // 记录数据
Form map[string]interface{} `json:"form"` // 表单数据
}

View file

@ -1,514 +0,0 @@
package workflow
import (
"fmt"
"path/filepath"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou"
gshare "github.com/yaoapp/gou/query/share"
"github.com/yaoapp/kun/any"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/kun/maps"
"github.com/yaoapp/kun/str"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/helper"
"github.com/yaoapp/yao/share"
)
// WARNING: the Workflow widget will be removed from yao engine
// Process
// 读取工作流 xiang.workflow.Find(name, workflow_id)
// 读取工作流 xiang.workflow.Open(name, uid, data_id)
// 保存工作流 xiang.workflow.Save(name, uid, node_name, data_id, input, ...output)
// 进入下一个节点 xiang.workflow.Next(name, uid, workflow_id, output)
// 跳转到指定节点 xiang.workflow.Goto(name, uid, workflow_id, node_name, output)
// 更新工作流状态 xiang.workflow.Status(name, uid, workflow_id, status_name, output)
// 标记结束流程 xiang.workflow.Done(name, uid, workflow_id, output)
// 标记关闭流程 xiang.workflow.Close(name, uid, workflow_id, output)
// 标记重置流程 xiang.workflow.Reset(name, uid, workflow_id, output)
// API:
// 读取工作流 GET /api/xiang/workflow/<工作流名称>/find/:id
// 读取工作流 GET /api/xiang/workflow/<工作流名称>/open
// 读取工作流配置 GET /api/xiang/workflow/<工作流名称>/setting
// 调用自定义API POST /api/xiang/workflow/<工作流名称>/<自定义API路由>
// WorkFlows 工作流列表
var WorkFlows = map[string]*WorkFlow{}
// Load 加载数据表格
func Load(cfg config.Config) {
LoadFrom(filepath.Join(cfg.Root, "workflows"), "")
}
// LoadFrom 从特定目录加载
func LoadFrom(dir string, prefix string) {
if share.DirNotExists(dir) {
return
}
share.Walk(dir, ".json", func(root, filename string) {
name := prefix + share.SpecName(root, filename)
content := share.ReadFile(filename)
_, err := LoadWorkFlow(content, name)
if err != nil {
log.With(log.F{"root": root, "filename": filename}).Error(err.Error())
}
})
}
// LoadWorkFlow 载入工作流
func LoadWorkFlow(source []byte, name string) (*WorkFlow, error) {
workflow := WorkFlow{Name: name, Source: source}
err := jsoniter.Unmarshal(source, &workflow)
if err != nil {
log.With(log.F{"name": name, "source": source}).Error("LoadWorkFlow: %s", err.Error())
return nil, err
}
workflow.SetupAPIs()
WorkFlows[workflow.Name] = &workflow
return WorkFlows[workflow.Name], nil
}
// Select 读取已加载图表
func Select(name string) *WorkFlow {
workflow, has := WorkFlows[name]
if !has {
exception.New(
fmt.Sprintf("工作流:%s; 尚未加载", name),
400,
).Throw()
}
return workflow
}
// Reload 重新载入工作流
func (workflow *WorkFlow) Reload() *WorkFlow {
new, err := LoadWorkFlow(workflow.Source, workflow.Name)
if err != nil {
exception.New(
fmt.Sprintf("工作流:%s; 加载失败", workflow.Name),
400,
).Throw()
}
WorkFlows[workflow.Name] = new
return new
}
// Find 读取给定ID的工作流
// uid 当前处理人ID, id 数据ID
func (workflow *WorkFlow) Find(id int) map[string]interface{} {
wflow := gou.Select("xiang.workflow")
res := wflow.MustFind(id, gou.QueryParam{
Select: []interface{}{
"data_id", "id", "input", "output", "name",
"user_id", "users",
"node_name", "node_status",
"status",
"updated_at", "created_at",
}})
return res
}
// Open 读取当前工作流(未完成的)
// uid 当前处理人ID, id 数据ID
func (workflow *WorkFlow) Open(uid int, id interface{}) map[string]interface{} {
wflow := gou.Select("xiang.workflow")
params := gou.QueryParam{
Select: []interface{}{
"data_id", "id", "input", "output", "name",
"user_id", "users",
"node_name", "node_status",
"status",
"updated_at", "created_at",
},
Wheres: []gou.QueryWhere{
{Column: "name", Value: workflow.Name},
{Column: "data_id", Value: id},
{Column: "user_ids", OP: "like", Value: fmt.Sprintf("%%|%d|%%", uid)},
{Column: "status", Value: "进行中"},
},
Limit: 1,
}
rows := wflow.MustGet(params)
if len(rows) > 0 {
return rows[0]
}
if workflow.Len() == 0 {
exception.New("工作流没有任何节点", 400).Throw()
}
users := map[string]interface{}{}
users[workflow.Nodes[0].Name] = uid
return map[string]interface{}{
"name": workflow.Name,
"data_id": id,
"node_name": workflow.Nodes[0].Name,
"user_id": uid,
"users": users,
"status": "进行中",
"node_status": "进行中",
"input": map[string]interface{}{},
"output": map[string]interface{}{},
}
}
// Setting 返回配置信息
// uid 当前处理人ID, id 数据ID
func (workflow *WorkFlow) Setting(uid int, id interface{}) map[string]interface{} {
wflow := workflow.Open(1, 1)
data := maps.MapStr{
"$in": wflow["input"],
"$input": wflow["input"],
"$out": wflow["output"],
"$outupt": wflow["output"],
"$data": wflow["output"],
}
nodes := workflow.FlowNodes(data.Dot())
_, idx := workflow.pickNode(string(str.Of(wflow["node_name"])))
return map[string]interface{}{
"nodes": nodes,
"node": nodes[idx],
"node_index": idx,
"read": true,
"write": any.Of(wflow["user_id"]).CInt() == uid,
"actions": workflow.Actions,
"name": workflow.Name,
"version": workflow.Version,
"label": workflow.Label,
"decription": workflow.Decription,
}
}
// Save 保存工作流节点数据 此版本使用Like实现
// uid 当前处理人ID, id 数据ID
func (workflow *WorkFlow) Save(uid int, name string, id interface{}, input Input, outputs ...map[string]interface{}) map[string]interface{} {
wflow := gou.Select("xiang.workflow")
params := gou.QueryParam{
Select: []interface{}{"id", "input", "output", "users"},
Wheres: []gou.QueryWhere{
{Column: "name", Value: workflow.Name},
{Column: "data_id", Value: id},
{Column: "user_ids", OP: "like", Value: fmt.Sprintf("%%|%d|%%", uid)},
{Column: "status", Value: "进行中"},
},
Limit: 1,
}
rows := wflow.MustGet(params)
data := map[string]interface{}{
"name": workflow.Name,
"data_id": id,
"node_name": name,
"user_id": uid,
}
users := map[string]interface{}{}
users[name] = uid
output := map[string]interface{}{}
nodeInput := map[string]interface{}{}
nodeInput[name] = input
if len(outputs) > 0 {
output = outputs[0]
}
if len(rows) > 0 {
data["id"] = rows[0].Get("id")
nodeInput = workflow.MergeData(rows[0].Get("input"), nodeInput)
users = workflow.MergeUsers(rows[0].Get("users"), users)
output = workflow.MergeData(rows[0].Get("output"), output)
} else {
data["status"] = "进行中"
data["node_status"] = "进行中"
}
data["users"] = users
data["user_ids"] = workflow.UserIDs(users)
data["input"] = nodeInput
data["output"] = output
id = wflow.MustSave(data)
return wflow.MustFind(id, gou.QueryParam{})
}
// Done 标记完成
// uid 当前处理人ID, id 工作流ID
func (workflow *WorkFlow) Done(uid int, id int, output map[string]interface{}) map[string]interface{} {
return workflow.Status(uid, id, "已完成", output)
}
// Close 标记关闭
// uid 当前处理人ID, id 工作流ID
func (workflow *WorkFlow) Close(uid int, id int, output map[string]interface{}) map[string]interface{} {
return workflow.Status(uid, id, "已关闭", output)
}
// Status 设定状态
// uid 当前处理人ID, id 工作流ID
func (workflow *WorkFlow) Status(uid int, id int, status string, output map[string]interface{}) map[string]interface{} {
wflow := workflow.Find(id)
mod := gou.Select("xiang.workflow")
output = workflow.MergeData(wflow["output"], output)
mod.Save(map[string]interface{}{
"id": wflow["id"],
"output": output,
"status": status,
})
return workflow.Find(id)
}
// Reset 重新开始
// uid 当前处理人ID, id 工作流ID
func (workflow *WorkFlow) Reset(uid int, id int, output map[string]interface{}) map[string]interface{} {
return workflow.Goto(uid, id, workflow.Nodes[0].Name, output)
}
// Goto 工作流跳转
func (workflow *WorkFlow) Goto(uid int, id int, name string, output map[string]interface{}) map[string]interface{} {
wflow := workflow.Find(id)
users := map[string]interface{}{}
curr, _ := workflow.pickNode(wflow["node_name"].(string))
users[curr.Name] = uid
node, _ := workflow.pickNode(name)
output = workflow.MergeData(wflow["output"], output)
users = workflow.MergeUsers(wflow["users"], users)
nodeUID := node.GetUID(users)
mod := gou.Select("xiang.workflow")
mod.Save(map[string]interface{}{
"id": wflow["id"],
"output": output,
"node_name": name,
"node_status": "进行中",
"user_id": nodeUID,
"users": users,
"user_ids": workflow.UserIDs(users),
})
return workflow.Find(id)
}
// Next 下一个工作流
// uid 当前处理人ID, id 工作流ID
func (workflow *WorkFlow) Next(uid int, id int, output map[string]interface{}) map[string]interface{} {
wflow := workflow.Find(id)
currNode, ok := wflow["node_name"].(string)
if !ok {
exception.New("流程数据异常: 当前节点信息错误", 500).Ctx(currNode).Throw()
}
users := map[string]interface{}{}
users[currNode] = uid
output = workflow.MergeData(wflow["output"], output)
// 读取下一个节点
data := map[string]interface{}{
"$in": wflow["input"],
"$input": wflow["input"],
"$out": output,
"$outupt": output,
"$data": output,
}
nextNode := workflow.nextNode(currNode, data)
nextUID := nextNode.MakeUID()
users[nextNode.Name] = nextUID
// 更新数据
users = workflow.MergeUsers(wflow["users"], users)
mod := gou.Select("xiang.workflow")
mod.Save(map[string]interface{}{
"id": wflow["id"],
"output": output,
"node_name": nextNode.Name,
"node_status": "进行中",
"user_id": nextUID,
"users": users,
"user_ids": workflow.UserIDs(users),
})
return workflow.Find(id)
}
// GetUID 读取节点相关人
func (node *Node) GetUID(users interface{}) int {
if users, ok := users.(map[string]interface{}); ok {
if uid, has := users[node.Name]; has {
return any.Of(uid).CInt()
}
}
return node.MakeUID()
}
// MakeUID 根据条件选择节点处理人
func (node *Node) MakeUID() int {
res := gou.NewProcess(node.User.Process, node.User.Args...).Run()
return any.Of(res).CInt()
}
func (workflow *WorkFlow) pickNode(name string) (*Node, int) {
for i, node := range workflow.Nodes {
if node.Name == name {
return &node, i
}
}
exception.New("流程数据异常: 未找到节点 %s", 500, name).Throw()
return nil, 0
}
// nextNode 查找下一个节点
func (workflow *WorkFlow) nextNode(currentNode string, data map[string]interface{}) *Node {
curr, index := workflow.pickNode(currentNode)
nextIndex := index + 1
if nextIndex == workflow.Len() {
exception.New("流程数据异常: 当前节点为最后一个节点", 500).Ctx(currentNode).Throw()
}
// 未声明 Next 节点, 转到下一个节点
if curr.Next == nil {
return &workflow.Nodes[nextIndex]
}
// 声明 Next 节点, 按条件到指定节点
data = maps.Of(data).Dot()
for _, next := range curr.Next {
node := workflow.GetNodeWhen(next, data)
if node != nil {
return node
}
}
exception.New("流程数据异常: 未找到符合条件的工作流节点", 500).Ctx(map[string]interface{}{"current": currentNode, "data": data}).Throw()
return nil
}
// GetNodeWhen 读取节点
func (workflow *WorkFlow) GetNodeWhen(next Next, data map[string]interface{}) *Node {
nextNode := ""
conditions := workflow.Conditions(next.Conditions, data)
if helper.When(conditions) {
nextNode = next.Goto
for i := 0; i < workflow.Len(); i++ {
node := workflow.Nodes[i]
if node.Name == nextNode {
return &node
}
}
}
return nil
}
// Conditions 处理绑定参数
func (workflow *WorkFlow) Conditions(conds []helper.Condition, data map[string]interface{}) []helper.Condition {
conditions := []helper.Condition{}
for _, cond := range conds {
if left, ok := cond.Left.(string); ok {
cond.Left = gshare.Bind(left, data)
}
if right, ok := cond.Right.(string); ok {
cond.Right = gshare.Bind(right, data)
}
conditions = append(conditions, cond)
}
return conditions
}
// UserIDs 读取用户ID
func (workflow *WorkFlow) UserIDs(users map[string]interface{}) string {
userIDs := []string{}
for _, u := range users {
userIDs = append(userIDs, fmt.Sprintf("|%d|", u))
}
userIDs = helper.ArrayStringUnique(userIDs)
return strings.Join(userIDs, ",")
}
// MergeUsers 合并数据
func (workflow *WorkFlow) MergeUsers(data interface{}, new interface{}) map[string]interface{} {
res, ok := data.(map[string]interface{})
if !ok {
return map[string]interface{}{}
}
if new, ok := new.(map[string]interface{}); ok {
for name, value := range new {
res[name] = value
}
}
return res
}
// MergeData 合并数据
func (workflow *WorkFlow) MergeData(data interface{}, new interface{}) map[string]interface{} {
res, ok := data.(map[string]interface{})
if !ok {
return map[string]interface{}{}
}
if new, ok := new.(map[string]interface{}); ok {
for key, value := range new {
res[key] = value
}
}
return res
}
// IsLastNode 检查是否为最后一个节点
func (workflow *WorkFlow) IsLastNode(name string) bool {
length := workflow.Len()
return workflow.Nodes[length-1].Name == name
}
// FlowNodes 转换为 flow
func (workflow *WorkFlow) FlowNodes(data map[string]interface{}) []map[string]interface{} {
res := []map[string]interface{}{}
nodes, err := jsoniter.Marshal(workflow.Nodes)
if err != nil {
exception.New("JSON解析错误 %s", 500, nodes).Throw()
}
err = jsoniter.Unmarshal(nodes, &res)
if err != nil {
exception.New("JSON解析错误 %s", 500, nodes).Throw()
}
nameMaps := map[string]int{}
for i, node := range workflow.Nodes {
nameMaps[node.Name] = i
}
for i, node := range res {
v := gshare.Bind(node, data)
if v, ok := v.(map[string]interface{}); ok {
res[i] = v
}
res[i]["id"] = i + 1
res[i]["label"] = res[i]["name"]
delete(res[i], "user")
delete(res[i], "next")
if workflow.Nodes[i].Next != nil {
for _, next := range workflow.Nodes[i].Next {
name := next.Goto
if id, has := nameMaps[name]; has {
res[id]["source"] = i + 1
}
}
}
}
return res
}
// Len 节点数量
func (workflow *WorkFlow) Len() int {
return len(workflow.Nodes)
}
// InputOf 映射表转换为Input
func InputOf(in map[string]interface{}) Input {
input := Input{Data: map[string]interface{}{}, Form: map[string]interface{}{}}
if data, ok := in["data"].(map[string]interface{}); ok {
input.Data = data
}
if form, ok := in["form"].(map[string]interface{}); ok {
input.Form = form
}
return input
}

View file

@ -1,296 +0,0 @@
package workflow
// WARNING: the Workflow widget will be removed from yao engine
// func init() {
// share.DBConnect(config.Conf.DB)
// share.Load(config.Conf)
// model.Load(config.Conf)
// engineModels := path.Join(os.Getenv("YAO_DEV"), "xiang", "models")
// model.LoadFrom(engineModels, "xiang.")
// query.Load(config.Conf)
// flow.Load(config.Conf)
// Load(config.Conf)
// }
// func TestLoad(t *testing.T) {
// share.DBConnect(config.Conf.DB)
// share.Load(config.Conf)
// model.Load(config.Conf)
// query.Load(config.Conf)
// Load(config.Conf)
// LoadFrom("not a path", "404.")
// check(t)
// }
// func TestSave(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// data := maps.Of(wflow).Dot()
// assert.Equal(t, int64(1), data.Get("id"))
// assert.Equal(t, "assign", data.Get("name"))
// assert.Equal(t, "选择商务负责人", data.Get("node_name"))
// assert.Equal(t, "进行中", data.Get("node_status"))
// assert.Equal(t, "进行中", data.Get("status"))
// assert.Equal(t, float64(1), data.Get("input.选择商务负责人.data.id"))
// assert.Equal(t, "云主机", data.Get("input.选择商务负责人.data.name"))
// assert.Equal(t, float64(1), data.Get("input.选择商务负责人.form.biz_id"))
// assert.Equal(t, "张良明", data.Get("input.选择商务负责人.form.name"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestSaveUpdate(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// wflow = assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云存储"},
// Form: map[string]interface{}{"biz_id": 1, "name": "李明博"},
// })
// data := maps.Of(wflow).Dot()
// assert.Equal(t, int64(1), data.Get("id"))
// assert.Equal(t, "assign", data.Get("name"))
// assert.Equal(t, "选择商务负责人", data.Get("node_name"))
// assert.Equal(t, "进行中", data.Get("node_status"))
// assert.Equal(t, "进行中", data.Get("status"))
// assert.Equal(t, float64(1), data.Get("input.选择商务负责人.data.id"))
// assert.Equal(t, "云存储", data.Get("input.选择商务负责人.data.name"))
// assert.Equal(t, float64(1), data.Get("input.选择商务负责人.form.biz_id"))
// assert.Equal(t, "李明博", data.Get("input.选择商务负责人.form.name"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestOpen(t *testing.T) {
// assignFlow := Select("assign")
// assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// wflow := assignFlow.Open(1, 1)
// data := maps.Of(wflow).Dot()
// assert.Equal(t, int64(1), data.Get("id"))
// assert.Equal(t, "assign", data.Get("name"))
// assert.Equal(t, "选择商务负责人", data.Get("node_name"))
// assert.Equal(t, "进行中", data.Get("node_status"))
// assert.Equal(t, "进行中", data.Get("status"))
// assert.Equal(t, int64(1), data.Get("user_id"))
// assert.Equal(t, float64(1), data.Get("users.选择商务负责人"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestOpenEmpty(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Open(1, 1)
// data := maps.Of(wflow).Dot()
// assert.Equal(t, false, data.Has("id"))
// assert.Equal(t, "assign", data.Get("name"))
// assert.Equal(t, "选择商务负责人", data.Get("node_name"))
// assert.Equal(t, "进行中", data.Get("node_status"))
// assert.Equal(t, "进行中", data.Get("status"))
// assert.Equal(t, 1, data.Get("user_id"))
// assert.Equal(t, 1, data.Get("users.选择商务负责人"))
// }
// func TestNext(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// wflow = assignFlow.Next(1, any.Of(wflow["id"]).CInt(), map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// data := maps.Of(wflow).Dot()
// assert.Equal(t, int64(1), data.Get("id"))
// assert.Equal(t, "assign", data.Get("name"))
// assert.Equal(t, "项目负责人审批", data.Get("node_name"))
// assert.Equal(t, "进行中", data.Get("node_status"))
// assert.Equal(t, "进行中", data.Get("status"))
// assert.Equal(t, int64(2), data.Get("user_id"))
// assert.Equal(t, true, data.Has("users"))
// assert.Equal(t, "测试项目", data.Get("output.项目名称"))
// assert.Equal(t, "林明波", data.Get("output.商务负责人名称"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestNextWhen(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// // Next When
// wflow = assignFlow.Next(2, id, map[string]interface{}{"审批结果": "通过"})
// data := maps.Of(wflow).Dot()
// assert.Equal(t, int64(1), data.Get("id"))
// assert.Equal(t, "assign", data.Get("name"))
// assert.Equal(t, "审批通过", data.Get("node_name"))
// assert.Equal(t, "进行中", data.Get("node_status"))
// assert.Equal(t, "进行中", data.Get("status"))
// assert.Equal(t, int64(1), data.Get("user_id"))
// assert.Equal(t, true, data.Has("users"))
// assert.Equal(t, "测试项目", data.Get("output.项目名称"))
// assert.Equal(t, "林明波", data.Get("output.商务负责人名称"))
// assert.Equal(t, "通过", data.Get("output.审批结果"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestGoto(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// wflow = assignFlow.Goto(2, id, "选择商务负责人", map[string]interface{}{"审批结果": "驳回"})
// data := maps.Of(wflow).Dot()
// assert.Equal(t, true, data.Has("id"))
// assert.Equal(t, "assign", data.Get("name"))
// assert.Equal(t, "选择商务负责人", data.Get("node_name"))
// assert.Equal(t, "进行中", data.Get("node_status"))
// assert.Equal(t, "进行中", data.Get("status"))
// assert.Equal(t, int64(1), data.Get("user_id"))
// assert.Equal(t, float64(1), data.Get("users.选择商务负责人"))
// assert.Equal(t, float64(2), data.Get("users.项目负责人审批"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestReset(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// wflow = assignFlow.Reset(2, id, map[string]interface{}{"审批结果": "驳回"})
// data := maps.Of(wflow).Dot()
// assert.Equal(t, true, data.Has("id"))
// assert.Equal(t, "assign", data.Get("name"))
// assert.Equal(t, "选择商务负责人", data.Get("node_name"))
// assert.Equal(t, "进行中", data.Get("node_status"))
// assert.Equal(t, "进行中", data.Get("status"))
// assert.Equal(t, int64(1), data.Get("user_id"))
// assert.Equal(t, float64(1), data.Get("users.选择商务负责人"))
// assert.Equal(t, float64(2), data.Get("users.项目负责人审批"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestDone(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// wflow = assignFlow.Done(2, id, map[string]interface{}{"关闭原因": "测试完成"})
// data := maps.Of(wflow).Dot()
// assert.Equal(t, "已完成", data.Get("status"))
// assert.Equal(t, "测试完成", data.Get("output.关闭原因"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestClose(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// wflow = assignFlow.Close(2, id, map[string]interface{}{"关闭原因": "测试关闭"})
// data := maps.Of(wflow).Dot()
// assert.Equal(t, "已关闭", data.Get("status"))
// assert.Equal(t, "测试关闭", data.Get("output.关闭原因"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func TestSetting(t *testing.T) {
// assignFlow := Select("assign")
// wflow := assignFlow.Save(1, "选择商务负责人", 1, Input{
// Data: map[string]interface{}{"id": 1, "name": "云主机"},
// Form: map[string]interface{}{"biz_id": 1, "name": "张良明"},
// })
// setting := assignFlow.Setting(1, 1)
// data := maps.Of(setting).Dot()
// assert.Equal(t, true, data.Get("read"))
// assert.Equal(t, "选择商务负责人", data.Get("node.label"))
// assert.Equal(t, true, data.Get("write"))
// id := any.Of(wflow["id"]).CInt()
// assignFlow.Next(1, id, map[string]interface{}{
// "项目名称": "测试项目",
// "商务负责人名称": "林明波",
// })
// setting = assignFlow.Setting(1, 1)
// data = maps.Of(setting).Dot()
// assert.Equal(t, true, data.Get("read"))
// assert.Equal(t, false, data.Get("write"))
// assert.Equal(t, "项目负责人审批", data.Get("node.label"))
// assert.Equal(t, 2, data.Get("nodes.2.source"))
// assert.Equal(t, 2, data.Get("nodes.3.source"))
// assert.Equal(t, "assign", data.Get("name"))
// assert.Equal(t, "指派商务负责人", data.Get("label"))
// // 清理数据
// capsule.Query().From("xiang_workflow").Truncate()
// }
// func check(t *testing.T) {
// keys := []string{}
// for key, workflow := range WorkFlows {
// keys = append(keys, key)
// workflow.Reload()
// }
// assert.Equal(t, 1, len(keys))
// }

View file

@ -1,133 +0,0 @@
{
"name": "工作流",
"table": {
"name": "xiang_workflow",
"comment": "工作流数据表",
"engine": "InnoDB"
},
"columns": [
{ "label": "ID", "name": "id", "type": "ID" },
{
"label": "名称",
"comment": "工作流名称",
"name": "name",
"type": "string",
"length": 255,
"index": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
}
]
},
{
"label": "数据标识",
"comment": "关联数据标识, 关联数据模型表格ID",
"name": "data_id",
"type": "bigInteger",
"index": true,
"nullable": true
},
{
"label": "节点",
"comment": "当前节点名称",
"name": "node_name",
"type": "string",
"length": 255,
"index": true,
"nullable": true
},
{
"label": "节点状态",
"comment": "当前节点状态",
"name": "node_status",
"type": "enum",
"default": "未开始",
"option": ["未开始", "进行中", "已完成"],
"index": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
},
{
"method": "enum",
"args": ["未开始", "进行中", "已完成"],
"message": "{{input}}不在许可范围, {{label}}应该为 未开始/进行中/已完成"
}
]
},
{
"label": "当前处理人",
"comment": "当前处理人",
"name": "user_id",
"type": "bigInteger",
"index": true,
"nullable": true
},
{
"label": "处理人IDs",
"comment": "相关处理人ID列表(用于检索)",
"name": "user_ids",
"type": "string",
"length": 255,
"index": true,
"nullable": true
},
{
"label": "处理人",
"comment": "相关处理人",
"name": "users",
"type": "json",
"nullable": true
},
{
"label": "数据",
"comment": "各节点输入数据(Key-Value)",
"name": "input",
"type": "json",
"nullable": true
},
{
"label": "处理结果",
"comment": "各节点处理结果(Key-Value)",
"name": "output",
"type": "json",
"nullable": true
},
{
"label": "操作记录",
"comment": "操作记录",
"name": "history",
"type": "json",
"nullable": true
},
{
"label": "状态",
"comment": "流程状态",
"name": "status",
"type": "enum",
"default": "未开始",
"option": ["未开始", "进行中", "已完成", "已关闭"],
"index": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}}类型错误, {{label}}应该为字符串"
},
{
"method": "enum",
"args": ["未开始", "进行中", "已完成", "已关闭"],
"message": "{{input}}不在许可范围, {{label}}应该为 未开始/进行中/已完成/已关闭"
}
]
}
],
"indexes": [],
"option": { "timestamps": true, "soft_deletes": true }
}