feat(base): add +dashboard-block-batch-create shortcut for creating multiple dashboard blocks in one call

Introduces BaseDashboardBlockBatchCreate shortcut that accepts a JSON array of
block specs and creates them sequentially via a single CLI invocation. Includes
local data_config validation, compact output summary, dry-run support, unit
tests, e2e dry-run test, and updated skill docs to prefer batch over repeated
single-block creates when two or more blocks are needed.
This commit is contained in:
yballul-bytedance
2026-06-30 21:47:00 +08:00
parent 75926f9744
commit b438137109
9 changed files with 357 additions and 22 deletions

View File

@@ -395,6 +395,73 @@ func TestBaseDashboardBlockExecuteCreate(t *testing.T) {
})
}
// TestBaseDashboardBlockExecuteBatchCreate tests the +dashboard-block-batch-create command.
func TestBaseDashboardBlockExecuteBatchCreate(t *testing.T) {
t.Run("creates blocks sequentially with compact output", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_stat",
"name": "订单数",
"type": "statistics",
"data_config": map[string]interface{}{
"table_name": "订单表",
"count_all": true,
},
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_line",
"name": "月度趋势",
"type": "line",
"data_config": map[string]interface{}{
"table_name": "订单表",
},
},
},
})
blocks := `[
{"name":"订单数","type":"statistics","data_config":{"table_name":"订单表","count_all":true}},
{"name":"月度趋势","type":"line","data_config":{"table_name":"订单表","series":[{"field_name":"金额","rollup":"sum"}],"group_by":[{"field_name":"月份","mode":"integrated","sort":{"type":"group","order":"asc"}}]}}
]`
args := []string{"+dashboard-block-batch-create", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--blocks", blocks}
if err := runShortcut(t, BaseDashboardBlockBatchCreate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"created_count": 2`) || !strings.Contains(got, `"blk_stat"`) || !strings.Contains(got, `"blk_line"`) {
t.Fatalf("stdout=%s", got)
}
if strings.Contains(got, "data_config") {
t.Fatalf("batch output should stay compact, stdout=%s", got)
}
})
t.Run("rejects invalid block data config", func(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-block-batch-create", "--base-token", "app_x", "--dashboard-id", "dsh_001",
"--blocks", `[{"name":"Bad","type":"column","data_config":{"series":[{"field_name":"金额","rollup":"COUNTA"}]}}]`,
}
err := runShortcut(t, BaseDashboardBlockBatchCreate, args, factory, stdout)
if err == nil {
t.Fatalf("expected validation error")
}
if got := err.Error(); !strings.Contains(got, "blocks[0].data_config") || !strings.Contains(got, "data_config 校验失败") {
t.Fatalf("unexpected error: %v", err)
}
})
}
// TestBaseDashboardBlockExecuteUpdate tests the +dashboard-block-update command.
func TestBaseDashboardBlockExecuteUpdate(t *testing.T) {
t.Run("update name and data-config", func(t *testing.T) {
@@ -596,6 +663,20 @@ func TestBaseDashboardBlockDryRun_Create(t *testing.T) {
}
}
// TestBaseDashboardBlockDryRun_BatchCreate tests the +dashboard-block-batch-create --dry-run flag.
func TestBaseDashboardBlockDryRun_BatchCreate(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
blocks := `[{"name":"订单数","type":"statistics","data_config":{"table_name":"订单表","count_all":true}},{"name":"月度趋势","type":"line","data_config":{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}],"group_by":[{"field_name":"月份","mode":"integrated","sort":{"type":"group","order":"asc"}}]}}]`
args := []string{"+dashboard-block-batch-create", "--base-token", "app_x", "--dashboard-id", "dsh_1", "--blocks", blocks, "--user-id-type", "open_id", "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardBlockBatchCreate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "POST /open-apis/base/v3/bases/app_x/dashboards/dsh_1/blocks") || !strings.Contains(got, "\"blocks\"") || !strings.Contains(got, "\"sequential\":true") || !strings.Contains(got, "open_id") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardBlockDryRun_Update tests the +dashboard-block-update --dry-run flag.
func TestBaseDashboardBlockDryRun_Update(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)

View File

@@ -170,7 +170,7 @@ func TestShortcutsCatalog(t *testing.T) {
"+form-questions-create", "+form-questions-delete", "+form-questions-update", "+form-questions-list",
"+form-submit",
"+dashboard-list", "+dashboard-get", "+dashboard-create", "+dashboard-update", "+dashboard-delete", "+dashboard-arrange",
"+dashboard-block-list", "+dashboard-block-get", "+dashboard-block-get-data", "+dashboard-block-create", "+dashboard-block-update", "+dashboard-block-delete",
"+dashboard-block-list", "+dashboard-block-get", "+dashboard-block-get-data", "+dashboard-block-create", "+dashboard-block-batch-create", "+dashboard-block-update", "+dashboard-block-delete",
}
if len(shortcuts) != len(want) {
t.Fatalf("len(shortcuts)=%d want=%d", len(shortcuts), len(want))
@@ -548,6 +548,19 @@ func TestBaseDashboardHelpGuidesAgents(t *testing.T) {
"sequentially",
},
},
{
name: "dashboard block batch create",
shortcut: BaseDashboardBlockBatchCreate,
wantTips: []string{
"lark-cli base +dashboard-block-batch-create --base-token <base_token> --dashboard-id <dashboard_id> --blocks",
"two or more blocks",
"sequentially",
"+table-list and +field-list",
"not table_id or field_id",
"dashboard-block-data-config.md as the SSOT",
"intentionally compact",
},
},
{
name: "dashboard block update",
shortcut: BaseDashboardBlockUpdate,

View File

@@ -0,0 +1,189 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package base
import (
"context"
"strconv"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/shortcuts/common"
)
type dashboardBlockCreateSpec struct {
Index int
Body map[string]interface{}
}
var BaseDashboardBlockBatchCreate = common.Shortcut{
Service: "base",
Command: "+dashboard-block-batch-create",
Description: "Create multiple blocks in a dashboard sequentially",
Risk: "write",
Scopes: []string{"base:dashboard:create"},
AuthTypes: authTypes(),
HasFormat: true,
Flags: []common.Flag{
baseTokenFlag(true),
dashboardIDFlag(true),
{Name: "blocks", Desc: "JSON array of block objects: [{\"name\":\"...\",\"type\":\"statistics\",\"data_config\":{...}}]. Use @file.json for long dashboards.", Required: true},
{Name: "user-id-type", Desc: "user ID type for user fields in filters: open_id / union_id / user_id"},
{Name: "no-validate", Type: "bool", Desc: "skip local data_config validation"},
},
Tips: []string{
`lark-cli base +dashboard-block-batch-create --base-token <base_token> --dashboard-id <dashboard_id> --blocks '[{"name":"Order Count","type":"statistics","data_config":{"table_name":"Orders","count_all":true}},{"name":"Monthly Sales","type":"line","data_config":{"table_name":"Orders","series":[{"field_name":"Amount","rollup":"SUM"}],"group_by":[{"field_name":"Month","mode":"integrated","sort":{"type":"group","order":"asc"}}]}}]'`,
"Use this when creating two or more blocks in the same dashboard; the CLI calls the platform sequentially for you.",
"Before creating data-backed blocks, use +table-list and +field-list to confirm real table and field names.",
"data_config uses table and field names, not table_id or field_id.",
"Read dashboard-block-data-config.md as the SSOT for chart templates, filters, metric rules, and type-specific fields; do not invent data_config from natural language.",
"The output is intentionally compact: created_count plus each created block's index, block_id, name, and type.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
_, err := parseDashboardBlockCreateSpecs(runtime)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
specs, _ := parseDashboardBlockCreateSpecs(runtime)
blocks := make([]interface{}, 0, len(specs))
for _, spec := range specs {
blocks = append(blocks, spec.Body)
}
params := map[string]interface{}{}
if uid := strings.TrimSpace(runtime.Str("user-id-type")); uid != "" {
params["user_id_type"] = uid
}
return common.NewDryRunAPI().
POST("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks").
Params(params).
Body(map[string]interface{}{"blocks": blocks, "sequential": true}).
Set("base_token", runtime.Str("base-token")).
Set("dashboard_id", runtime.Str("dashboard-id"))
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeDashboardBlockBatchCreate(runtime)
},
}
func parseDashboardBlockCreateSpecs(runtime *common.RuntimeContext) ([]dashboardBlockCreateSpec, error) {
pc := newParseCtx(runtime)
items, err := parseJSONArray(pc, runtime.Str("blocks"), "blocks")
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks must contain at least one block").WithParam("--blocks")
}
specs := make([]dashboardBlockCreateSpec, 0, len(items))
for index, item := range items {
obj, ok := item.(map[string]interface{})
if !ok {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d] must be a JSON object", index).WithParam("--blocks")
}
body, err := buildDashboardBlockCreateBody(runtime, obj, index)
if err != nil {
return nil, err
}
specs = append(specs, dashboardBlockCreateSpec{Index: index, Body: body})
}
return specs, nil
}
func buildDashboardBlockCreateBody(runtime *common.RuntimeContext, obj map[string]interface{}, index int) (map[string]interface{}, error) {
name, _ := obj["name"].(string)
name = strings.TrimSpace(name)
if name == "" {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].name is required", index).WithParam("--blocks")
}
blockType, _ := obj["type"].(string)
blockType = strings.TrimSpace(blockType)
if blockType == "" {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].type is required", index).WithParam("--blocks")
}
body := map[string]interface{}{
"name": name,
"type": blockType,
}
if rawConfig, ok := obj["data-config"]; ok {
obj["data_config"] = rawConfig
}
if rawConfig, ok := obj["data_config"]; ok {
cfg, err := dashboardBlockDataConfigFromSpec(rawConfig, index)
if err != nil {
return nil, err
}
if !runtime.Bool("no-validate") {
cfg = normalizeDataConfig(cfg)
if problems := validateBlockDataConfig(blockType, cfg); len(problems) > 0 {
return nil, formatDataConfigErrors(prefixDataConfigProblems(index, problems))
}
}
body["data_config"] = cfg
} else if strings.EqualFold(blockType, "text") && !runtime.Bool("no-validate") {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].data_config.text is required for text blocks", index).WithParam("--blocks")
}
return body, nil
}
func dashboardBlockDataConfigFromSpec(raw interface{}, index int) (map[string]interface{}, error) {
switch val := raw.(type) {
case map[string]interface{}:
return val, nil
case string:
var cfg map[string]interface{}
if err := common.ParseJSON([]byte(strings.TrimSpace(val)), &cfg); err != nil {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].data_config must be a JSON object or object-encoded string", index).WithParam("--blocks").WithCause(err)
}
if cfg == nil {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].data_config must be a JSON object", index).WithParam("--blocks")
}
return cfg, nil
default:
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].data_config must be a JSON object", index).WithParam("--blocks")
}
}
func prefixDataConfigProblems(index int, problems []string) []string {
prefixed := make([]string, 0, len(problems))
for _, problem := range problems {
prefixed = append(prefixed, "blocks["+strconv.Itoa(index)+"].data_config: "+problem)
}
return prefixed
}
func executeDashboardBlockBatchCreate(runtime *common.RuntimeContext) error {
specs, err := parseDashboardBlockCreateSpecs(runtime)
if err != nil {
return err
}
params := map[string]interface{}{}
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
params["user_id_type"] = userIDType
}
created := make([]interface{}, 0, len(specs))
for _, spec := range specs {
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "blocks"), params, spec.Body)
if err != nil {
return err
}
created = append(created, compactCreatedDashboardBlock(spec.Index, data))
}
runtime.Out(map[string]interface{}{
"created": true,
"created_count": len(created),
"dashboard_id": runtime.Str("dashboard-id"),
"blocks": created,
}, nil)
return nil
}
func compactCreatedDashboardBlock(index int, data map[string]interface{}) map[string]interface{} {
item := map[string]interface{}{"index": index}
for _, key := range []string{"block_id", "id", "name", "type"} {
if value, ok := data[key]; ok {
item[key] = value
}
}
return item
}

View File

@@ -93,6 +93,7 @@ func Shortcuts() []common.Shortcut {
BaseDashboardBlockGet,
BaseDashboardBlockGetData,
BaseDashboardBlockCreate,
BaseDashboardBlockBatchCreate,
BaseDashboardBlockUpdate,
BaseDashboardBlockDelete,
}

View File

@@ -64,7 +64,7 @@ metadata:
| 表单提交 | `+form-submit` | 先读 [lark-base-form-detail.md](references/lark-base-form-detail.md) 获取题目、filter 和附件所需 `base_token`;提交 JSON 读 [lark-base-form-submit.md](references/lark-base-form-submit.md) |
| 表单题目创建/更新 | `+form-questions-create` / `+form-questions-update` | 读 [lark-base-form-questions-create.md](references/lark-base-form-questions-create.md) / [lark-base-form-questions-update.md](references/lark-base-form-questions-update.md) |
| 其他表单管理 | `+form-list/get/detail/create/update/delete` / `+form-questions-list/delete` | `+form-detail` 读 [lark-base-form-detail.md](references/lark-base-form-detail.md);删除前确认目标表单 |
| 仪表盘与组件 | `+dashboard-*` / `+dashboard-block-*` | 提到图表/看板/block 时先读 [lark-base-dashboard.md](references/lark-base-dashboard.md);组件 `data_config` 读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md);读取图表计算结果用 `+dashboard-block-get-data` |
| 仪表盘与组件 | `+dashboard-*` / `+dashboard-block-*` | 提到图表/看板/block 时先读 [lark-base-dashboard.md](references/lark-base-dashboard.md)新建 2 个及以上组件优先用 `+dashboard-block-batch-create`组件 `data_config` 读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md);读取图表计算结果用 `+dashboard-block-get-data` |
| Workflow | `+workflow-*` | 创建/更新或理解 steps 时读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) 和 steps JSON SSOT [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md)list/get/enable/disable 只处理 workflow ID 与启停状态 |
| 高级权限与角色 | `+advperm-*` / `+role-*` | 角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md);角色 create/update 或解读完整配置再读权限 JSON SSOT [role-config.md](references/role-config.md);系统角色不可删除;关闭高级权限会影响自定义角色 |
@@ -122,7 +122,7 @@ metadata:
## Dashboard / Workflow / Role
- Dashboard 的复杂点是 block 的 `data_config`,不是 list/get/create/delete 命令参数。创建或更新 block 前先读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md),组件必须串行创建;`+dashboard-arrange` 是服务端智能布局,只在用户明确要求重排/美化时执行。`+dashboard-block-get-data` 读取图表最终计算结果,不返回 block 名称、类型、布局或 `data_config`;需要元数据先用 `+dashboard-block-get`
- Dashboard 的复杂点是 block 的 `data_config`,不是 list/get/create/delete 命令参数。创建或更新 block 前先读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md)。需要新建 2 个及以上组件时优先用 `+dashboard-block-batch-create --blocks '[...]'`,由 CLI 内部串行创建并返回紧凑摘要;只建 1 个组件、调试单个配置或修复某个失败组件时再用 `+dashboard-block-create``+dashboard-arrange` 是服务端智能布局,只在用户明确要求重排/美化时执行。`+dashboard-block-get-data` 读取图表最终计算结果,不返回 block 名称、类型、布局或 `data_config`;需要元数据先用 `+dashboard-block-get`
- Workflow 的复杂点是 `steps` 结构。创建、更新或解释完整 workflow 时读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) 和 steps JSON SSOT [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md)enable/disable/list 只需确认 workflow ID、当前启停状态和用户意图。
- Role 的复杂点是权限 JSON。角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md)`+role-create` 只支持自定义角色;`+role-update` 是 delta merge角色 create/update 或解读完整配置时读权限 JSON SSOT [role-config.md](references/role-config.md)。`+role-delete` 只适用于自定义角色,系统角色不可删除;删除角色和关闭高级权限前必须确认目标和影响。

View File

@@ -36,6 +36,8 @@ user / created_by / updated_by: is, isNot, isEmpty, isNotEmpty
## data_config 通用结构
创建多个组件时,把每个组件写成 `{"name":"...","type":"...","data_config":{...}}`,整体放入 `+dashboard-block-batch-create --blocks '[...]'`。只创建或修复单个组件时,才把本页模板直接传给 `+dashboard-block-create --data-config '{...}'`
| 字段 | 类型 | 说明 |
|------|------|------|
| `table_name` | string | 关联数据表名称 |

View File

@@ -15,7 +15,7 @@ Dashboard 是 Base 中的数据可视化看板,可以把表格数据变成**
| 你想做什么 | 用这些命令 | 关键文档 |
|------|-----------|---------|
| 创建/删除/改名称 | `+dashboard-create/delete/update` | 本页下方「仪表盘管理」 |
| 在仪表盘里添加组件 | `+dashboard-block-create` | 先定位 dashboard、表和字段再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 构造 `data_config` |
| 在仪表盘里添加组件 | `+dashboard-block-batch-create` / `+dashboard-block-create` | 新建 2 个及以上组件优先用 batch只建 1 个组件或修复单个组件时用 create。先定位 dashboard、表和字段再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 构造 `data_config` |
| 修改组件 | `+dashboard-block-update` | 先读 block 现状,再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 决定替换哪些顶层 key |
| 查看仪表盘有哪些组件 | `+dashboard-get``+dashboard-block-list` | 本页下方「查看仪表盘」 |
| 读取图表计算结果 | `+dashboard-block-get-data` | 返回图表最终数据协议;需要 block 元数据先用 `+dashboard-block-get` |
@@ -39,27 +39,18 @@ lark-cli base +field-list --base-token xxx --table-id <table_id>
# 第 3 步:规划应该创建哪些组件(根据用户需求确定组件类型和数量)
# 例如:总销售额(指标卡)、月度趋势(折线图)、品类占比(饼图)
# 第 4 步:顺序创建个组件(必须串行执行,不能并发)
# 第 4 步:批量创建个组件(CLI 内部会串行执行,不能并发)
# 重要:创建组件前,先确定 dashboard_id、组件 name/type 和真实表字段
# 再阅读 dashboard-block-data-config.md 了解 data_config 结构、组件类型和 filter 规则
# 第 1 个组件
lark-cli base +dashboard-block-create \
# 创建 2 个及以上组件时优先用 batch避免多轮工具调用和重复返回
lark-cli base +dashboard-block-batch-create \
--base-token xxx \
--dashboard-id blk_xxx \
--name "总销售额" \
--type statistics \
--data-config '{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}]}'
# 第 2 个组件(等上一个完成后再执行)
lark-cli base +dashboard-block-create \
--base-token xxx \
--dashboard-id blk_xxx \
--name "月度趋势" \
--type line \
--data-config '{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}],"group_by":[{"field_name":"月份","mode":"integrated"}]}'
# 继续创建其他组件...
--blocks '[
{"name":"总销售额","type":"statistics","data_config":{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}]}},
{"name":"月度趋势","type":"line","data_config":{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}],"group_by":[{"field_name":"月份","mode":"integrated","sort":{"type":"group","order":"asc"}}]}}
]'
# 第 5 步:组件创建完成后,使用 arrange 命令智能重排布局(可选但推荐)
# 默认布局可能不够美观arrange 会根据组件数量和类型自动优化布局
@@ -83,9 +74,17 @@ lark-cli base +dashboard-get --base-token xxx --dashboard-id blk_xxx
lark-cli base +table-list --base-token xxx
lark-cli base +field-list --base-token xxx --table-id <table_id>
# 第 4 步:顺序创建每个新组件(必须串行执行,不能并发)
# 第 4 步:创建新组件
# 重要:先确定 dashboard_id、组件 name/type 和真实表字段
# 再阅读 dashboard-block-data-config.md 了解 data_config 结构
# 新建 2 个及以上组件时优先用 batch
lark-cli base +dashboard-block-batch-create \
--base-token xxx \
--dashboard-id blk_xxx \
--blocks '[{"name":"新组件名","type":"column","data_config":{...}}]'
# 只新建 1 个组件时可用单组件命令
lark-cli base +dashboard-block-create \
--base-token xxx \
--dashboard-id blk_xxx \
@@ -207,7 +206,7 @@ A: 常见原因:
- 组件创建并发执行(必须串行,等上一个完成再执行下一个)
**Q: 可以一次创建多个组件吗?**
A: 可以,必须串行执行。等上一个 `+dashboard-block-create` 完成后再执行下一个
A: 可以。使用 `+dashboard-block-batch-create --blocks '[...]'`CLI 会在一个命令内按顺序串行创建多个组件,并返回每个新组件的 `block_id/name/type` 摘要。不要并发调用多个 `+dashboard-block-create` 写同一个 dashboard只创建 1 个组件或修复单个失败组件时再用 `+dashboard-block-create`
**Q: 组件的 `type` 创建后能改吗?**
A: 不能。`+dashboard-block-update` 只能修改 `name``data_config`,不能修改 `type`

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package base
import (
"context"
"strings"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBaseDashboardBlockBatchCreateDryRun(t *testing.T) {
setBaseDryRunConfigEnv(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
blocks := `[{"name":"订单数","type":"statistics","data_config":{"table_name":"订单表","count_all":true}},{"name":"月度趋势","type":"line","data_config":{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}],"group_by":[{"field_name":"月份","mode":"integrated","sort":{"type":"group","order":"asc"}}]}}]`
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"base", "+dashboard-block-batch-create",
"--base-token", "app_x",
"--dashboard-id", "dsh_1",
"--blocks", blocks,
"--user-id-type", "open_id",
"--dry-run",
},
BinaryPath: "../../../lark-cli",
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
output := strings.TrimSpace(result.Stdout)
assert.Contains(t, output, "/open-apis/base/v3/bases/app_x/dashboards/dsh_1/blocks")
assert.Contains(t, output, `"method": "POST"`)
assert.Contains(t, output, `"base_token": "app_x"`)
assert.Contains(t, output, `"dashboard_id": "dsh_1"`)
assert.Contains(t, output, `"user_id_type": "open_id"`)
assert.Contains(t, output, `"blocks"`)
assert.Contains(t, output, `"sequential": true`)
assert.Contains(t, output, `"name": "订单数"`)
assert.Contains(t, output, `"type": "line"`)
}

View File

@@ -27,6 +27,7 @@
| ✓ | base +base-block-move | shortcut | base_block_dryrun_test.go::TestBaseBlockDryRun/move root,move after | `--base-token`; `--block-id`; optional `--parent-id`; `--after-id`; dry-run only | request shape only |
| ✓ | base +base-block-rename | shortcut | base_block_dryrun_test.go::TestBaseBlockDryRun/rename | `--base-token`; `--block-id`; `--name`; dry-run only | request shape only |
| ✕ | base +dashboard-arrange | shortcut | | none | dashboard workflows not covered |
| ✓ | base +dashboard-block-batch-create | shortcut | base_dashboard_block_batch_create_dryrun_test.go::TestBaseDashboardBlockBatchCreateDryRun | `--base-token`; `--dashboard-id`; `--blocks`; dry-run only | request shape only |
| ✕ | base +dashboard-block-create | shortcut | | none | dashboard workflows not covered |
| ✕ | base +dashboard-block-delete | shortcut | | none | dashboard workflows not covered |
| ✕ | base +dashboard-block-get | shortcut | | none | dashboard workflows not covered |