Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions shortcuts/base/base_dashboard_execute_test.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion shortcuts/base/base_shortcuts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func TestShortcutsCatalog(t *testing.T) {
"+data-query",
"+form-create", "+form-delete", "+form-list", "+form-update", "+form-get",
"+form-questions-create", "+form-questions-delete", "+form-questions-update", "+form-questions-list",
"+dashboard-list", "+dashboard-get", "+dashboard-create", "+dashboard-update", "+dashboard-delete",
"+dashboard-list", "+dashboard-get", "+dashboard-create", "+dashboard-update", "+dashboard-delete", "+dashboard-arrange",
"+dashboard-block-list", "+dashboard-block-get", "+dashboard-block-create", "+dashboard-block-update", "+dashboard-block-delete",
}
if len(shortcuts) != len(want) {
Expand Down
29 changes: 29 additions & 0 deletions shortcuts/base/dashboard_arrange.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package base

import (
"context"

"github.com/larksuite/cli/shortcuts/common"
)

var BaseDashboardArrange = common.Shortcut{
Service: "base",
Command: "+dashboard-arrange",
Description: "Auto-arrange dashboard blocks layout (server-side smart layout)",
Risk: "write",
Scopes: []string{"base:dashboard:update"},
AuthTypes: authTypes(),
HasFormat: true,
Flags: []common.Flag{
baseTokenFlag(true),
dashboardIDFlag(true),
{Name: "user-id-type", Desc: "user ID type: open_id / union_id / user_id"},
},
DryRun: dryRunDashboardArrange,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeDashboardArrange(runtime)
},
}
9 changes: 7 additions & 2 deletions shortcuts/base/dashboard_block_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package base
import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/larksuite/cli/shortcuts/common"
Expand All @@ -23,7 +24,7 @@ var BaseDashboardBlockCreate = common.Shortcut{
baseTokenFlag(true),
dashboardIDFlag(true),
{Name: "name", Desc: "block name", Required: true},
{Name: "type", Desc: "block type: column(柱状图)|bar(条形图)|line(折线图)|pie(饼图)|ring(环形图)|area(面积图)|combo(组合图)|scatter(散点图)|funnel(漏斗图)|wordCloud(词云)|radar(雷达图)|statistics(指标卡). Read dashboard-block-data-config.md before creating.", Required: true},
{Name: "type", Desc: "block type: column(柱状图)|bar(条形图)|line(折线图)|pie(饼图)|ring(环形图)|area(面积图)|combo(组合图)|scatter(散点图)|funnel(漏斗图)|wordCloud(词云)|radar(雷达图)|statistics(指标卡)|text(文本). Read dashboard-block-data-config.md before creating.", Required: true},
{Name: "data-config", Desc: "data config JSON object (table_name, series, count_all, group_by, filter, etc.)"},
{Name: "user-id-type", Desc: "user ID type: open_id / union_id / user_id"},
{Name: "no-validate", Type: "bool", Desc: "skip local data_config validation"},
Expand All @@ -34,7 +35,11 @@ var BaseDashboardBlockCreate = common.Shortcut{
}
raw := runtime.Str("data-config")
if strings.TrimSpace(raw) == "" {
return nil // 允许无 data_config 的创建(某些类型可先创建后配置)
// text 类型必须提供 data-config(含 text 内容)
if strings.ToLower(runtime.Str("type")) == "text" {
return fmt.Errorf("text 类型组件必须提供 data-config,包含必填字段 text")
}
return nil
}
cfg, err := parseJSONObject(raw, "data-config")
if err != nil {
Expand Down
6 changes: 2 additions & 4 deletions shortcuts/base/dashboard_block_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var BaseDashboardBlockUpdate = common.Shortcut{
dashboardIDFlag(true),
blockIDFlag(true),
{Name: "name", Desc: "new block name"},
{Name: "data-config", Desc: "data config JSON: table_name, series|count_all (mutually exclusive), group_by, filter. See dashboard-block-data-config.md for details."},
{Name: "data-config", Desc: "data config JSON. For chart types: table_name, series|count_all, group_by, filter. For text type: text (markdown supported). See dashboard-block-data-config.md for details."},
{Name: "user-id-type", Desc: "user ID type: open_id / union_id / user_id"},
{Name: "no-validate", Type: "bool", Desc: "skip local data_config validation"},
},
Expand All @@ -41,9 +41,7 @@ var BaseDashboardBlockUpdate = common.Shortcut{
return err
}
norm := normalizeDataConfig(cfg)
if errs := validateBlockDataConfig("", norm); len(errs) > 0 { // update 时不强校验类型特性
return formatDataConfigErrors(errs)
}
// update 时不做强类型校验(不传 type),让后端验证具体字段
b, _ := json.Marshal(norm)
_ = runtime.Cmd.Flags().Set("data-config", string(b))
return nil
Expand Down
56 changes: 56 additions & 0 deletions shortcuts/base/dashboard_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@ import (
"github.com/larksuite/cli/shortcuts/common"
)

// dashboardIDFlag returns a Flag for dashboard ID.
func dashboardIDFlag(required bool) common.Flag {
return common.Flag{Name: "dashboard-id", Desc: "dashboard ID", Required: required}
}

// blockIDFlag returns a Flag for dashboard block ID.
func blockIDFlag(required bool) common.Flag {
return common.Flag{Name: "block-id", Desc: "dashboard block ID", Required: required}
}

// dryRunDashboardBase returns a base DryRunAPI with common dashboard parameters set.
func dryRunDashboardBase(runtime *common.RuntimeContext) *common.DryRunAPI {
return common.NewDryRunAPI().
Set("base_token", runtime.Str("base-token")).
Set("dashboard_id", runtime.Str("dashboard-id")).
Set("block_id", runtime.Str("block-id"))
}

// dryRunDashboardList returns a DryRunAPI for listing dashboards.
func dryRunDashboardList(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
params := map[string]interface{}{}
if pageSize := strings.TrimSpace(runtime.Str("page-size")); pageSize != "" {
Expand All @@ -38,11 +42,13 @@ func dryRunDashboardList(_ context.Context, runtime *common.RuntimeContext) *com
Params(params)
}

// dryRunDashboardGet returns a DryRunAPI for getting a dashboard.
func dryRunDashboardGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
return dryRunDashboardBase(runtime).
GET("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id")
}

// dryRunDashboardCreate returns a DryRunAPI for creating a dashboard.
func dryRunDashboardCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
body := map[string]interface{}{"name": runtime.Str("name")}
if themeStyle := strings.TrimSpace(runtime.Str("theme-style")); themeStyle != "" {
Expand All @@ -53,6 +59,7 @@ func dryRunDashboardCreate(_ context.Context, runtime *common.RuntimeContext) *c
Body(body)
}

// dryRunDashboardUpdate returns a DryRunAPI for updating a dashboard.
func dryRunDashboardUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
Expand All @@ -66,11 +73,13 @@ func dryRunDashboardUpdate(_ context.Context, runtime *common.RuntimeContext) *c
Body(body)
}

// dryRunDashboardDelete returns a DryRunAPI for deleting a dashboard.
func dryRunDashboardDelete(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
return dryRunDashboardBase(runtime).
DELETE("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id")
}

// dryRunDashboardBlockList returns a DryRunAPI for listing dashboard blocks.
func dryRunDashboardBlockList(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
params := map[string]interface{}{}
if pageSize := strings.TrimSpace(runtime.Str("page-size")); pageSize != "" {
Expand All @@ -84,6 +93,7 @@ func dryRunDashboardBlockList(_ context.Context, runtime *common.RuntimeContext)
Params(params)
}

// dryRunDashboardBlockGet returns a DryRunAPI for getting a dashboard block.
func dryRunDashboardBlockGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
params := map[string]interface{}{}
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
Expand All @@ -94,6 +104,7 @@ func dryRunDashboardBlockGet(_ context.Context, runtime *common.RuntimeContext)
Params(params)
}

// dryRunDashboardBlockCreate returns a DryRunAPI for creating a dashboard block.
func dryRunDashboardBlockCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
Expand All @@ -118,6 +129,7 @@ func dryRunDashboardBlockCreate(_ context.Context, runtime *common.RuntimeContex
Body(body)
}

// dryRunDashboardBlockUpdate returns a DryRunAPI for updating a dashboard block.
func dryRunDashboardBlockUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
Expand All @@ -138,13 +150,15 @@ func dryRunDashboardBlockUpdate(_ context.Context, runtime *common.RuntimeContex
Body(body)
}

// dryRunDashboardBlockDelete returns a DryRunAPI for deleting a dashboard block.
func dryRunDashboardBlockDelete(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
return dryRunDashboardBase(runtime).
DELETE("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks/:block_id")
}

// ── Dashboard CRUD ──────────────────────────────────────────────────

// executeDashboardList lists all dashboards in a base.
func executeDashboardList(runtime *common.RuntimeContext) error {
params := map[string]interface{}{}
if pageSize := strings.TrimSpace(runtime.Str("page-size")); pageSize != "" {
Expand All @@ -161,6 +175,7 @@ func executeDashboardList(runtime *common.RuntimeContext) error {
return nil
}

// executeDashboardGet retrieves a dashboard by ID.
func executeDashboardGet(runtime *common.RuntimeContext) error {
data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id")), nil, nil)
if err != nil {
Expand All @@ -170,6 +185,7 @@ func executeDashboardGet(runtime *common.RuntimeContext) error {
return nil
}

// executeDashboardCreate creates a new dashboard.
func executeDashboardCreate(runtime *common.RuntimeContext) error {
body := map[string]interface{}{"name": runtime.Str("name")}
if themeStyle := strings.TrimSpace(runtime.Str("theme-style")); themeStyle != "" {
Expand All @@ -183,6 +199,7 @@ func executeDashboardCreate(runtime *common.RuntimeContext) error {
return nil
}

// executeDashboardUpdate updates an existing dashboard.
func executeDashboardUpdate(runtime *common.RuntimeContext) error {
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
Expand All @@ -199,6 +216,7 @@ func executeDashboardUpdate(runtime *common.RuntimeContext) error {
return nil
}

// executeDashboardDelete deletes a dashboard by ID.
func executeDashboardDelete(runtime *common.RuntimeContext) error {
_, err := baseV3Call(runtime, "DELETE", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id")), nil, nil)
if err != nil {
Expand All @@ -210,6 +228,7 @@ func executeDashboardDelete(runtime *common.RuntimeContext) error {

// ── Dashboard Block CRUD ────────────────────────────────────────────

// executeDashboardBlockList lists all blocks in a dashboard.
func executeDashboardBlockList(runtime *common.RuntimeContext) error {
params := map[string]interface{}{}
if pageSize := strings.TrimSpace(runtime.Str("page-size")); pageSize != "" {
Expand All @@ -226,6 +245,7 @@ func executeDashboardBlockList(runtime *common.RuntimeContext) error {
return nil
}

// executeDashboardBlockGet retrieves a dashboard block by ID.
func executeDashboardBlockGet(runtime *common.RuntimeContext) error {
params := map[string]interface{}{}
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
Expand All @@ -239,6 +259,7 @@ func executeDashboardBlockGet(runtime *common.RuntimeContext) error {
return nil
}

// executeDashboardBlockCreate creates a new dashboard block.
func executeDashboardBlockCreate(runtime *common.RuntimeContext) error {
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
Expand Down Expand Up @@ -268,6 +289,7 @@ func executeDashboardBlockCreate(runtime *common.RuntimeContext) error {
return nil
}

// executeDashboardBlockUpdate updates an existing dashboard block.
func executeDashboardBlockUpdate(runtime *common.RuntimeContext) error {
body := map[string]interface{}{}
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
Expand All @@ -293,6 +315,7 @@ func executeDashboardBlockUpdate(runtime *common.RuntimeContext) error {
return nil
}

// executeDashboardBlockDelete deletes a dashboard block by ID.
func executeDashboardBlockDelete(runtime *common.RuntimeContext) error {
_, err := baseV3Call(runtime, "DELETE", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "blocks", runtime.Str("block-id")), nil, nil)
if err != nil {
Expand All @@ -301,3 +324,36 @@ func executeDashboardBlockDelete(runtime *common.RuntimeContext) error {
runtime.Out(map[string]interface{}{"deleted": true, "block_id": runtime.Str("block-id")}, nil)
return nil
}

// ── Dashboard Arrange ────────────────────────────────────────────────

// dryRunDashboardArrange returns a DryRunAPI for the dashboard arrange endpoint.
func dryRunDashboardArrange(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
params := map[string]interface{}{}
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
params["user_id_type"] = userIDType
}
return dryRunDashboardBase(runtime).
POST("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/arrange").
Params(params).
Body(map[string]interface{}{})
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

// executeDashboardArrange sends a POST request to auto-arrange dashboard blocks layout.
func executeDashboardArrange(runtime *common.RuntimeContext) error {
params := map[string]interface{}{}
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
params["user_id_type"] = userIDType
}
// 请求体为空对象,由服务端智能重排
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "arrange"), params, map[string]interface{}{})
if err != nil {
return err
}
if data == nil {
data = map[string]interface{}{}
}
data["arranged"] = true
Comment thread
greptile-apps[bot] marked this conversation as resolved.
runtime.Out(data, nil)
return nil
Comment thread
huangxincola marked this conversation as resolved.
}
15 changes: 15 additions & 0 deletions shortcuts/base/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,8 @@ func sleepBetweenBatches(index int, total int) {

// ── Dashboard Block data_config normalization & validation ───────────

// normalizeDataConfig normalizes data_config fields for dashboard blocks.
// It converts series[].rollup to uppercase and group_by[].sort fields to lowercase.
func normalizeDataConfig(cfg map[string]interface{}) map[string]interface{} {
if cfg == nil {
return nil
Expand Down Expand Up @@ -1014,8 +1016,21 @@ func normalizeDataConfig(cfg map[string]interface{}) map[string]interface{} {
return out
}

// validateBlockDataConfig validates data_config based on block type.
// For text type, it checks for the presence of text field.
// For chart types, it validates table_name, series/count_all, group_by, and filter fields.
func validateBlockDataConfig(blockType string, cfg map[string]interface{}) []string {
var errs []string

// text 类型特殊校验:只需要有 text 字段即可
if strings.ToLower(blockType) == "text" {
if txt, _ := cfg["text"].(string); strings.TrimSpace(txt) == "" {
errs = append(errs, "text 类型组件缺少必填字段 text")
}
return errs
}

// 图表类型通用校验
// table_name 必填
if tn, _ := cfg["table_name"].(string); strings.TrimSpace(tn) == "" {
errs = append(errs, "缺少必填字段 table_name")
Expand Down
1 change: 1 addition & 0 deletions shortcuts/base/shortcuts.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func Shortcuts() []common.Shortcut {
BaseDashboardCreate,
BaseDashboardUpdate,
BaseDashboardDelete,
BaseDashboardArrange,
BaseDashboardBlockList,
BaseDashboardBlockGet,
BaseDashboardBlockCreate,
Expand Down
2 changes: 1 addition & 1 deletion skills/lark-base/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,4 @@ https://{domain}/base/{base-token}?table={table-id}&view={view-id}
| [`form commands`](references/lark-base-form-create.md) | `+form-list / +form-get / +form-create / +form-update / +form-delete` |
| [`form questions commands`](references/lark-base-form-questions-create.md) | `+form-questions-list / +form-questions-create / +form-questions-update / +form-questions-delete` |
| [`workflow commands`](references/lark-base-workflow.md) | `+workflow-list / +workflow-get / +workflow-create / +workflow-update / +workflow-enable / +workflow-disable` |
| [`dashboard / dashboard-block commands`](references/lark-base-dashboard.md) | `+dashboard-list / +dashboard-get / +dashboard-create / +dashboard-update / +dashboard-delete / +dashboard-block-list / +dashboard-block-get / +dashboard-block-create / +dashboard-block-update / +dashboard-block-delete` |
| [`dashboard / dashboard-block commands`](references/lark-base-dashboard.md) | `+dashboard-list / +dashboard-get / +dashboard-create / +dashboard-update / +dashboard-delete / +dashboard-arrange / +dashboard-block-list / +dashboard-block-get / +dashboard-block-create / +dashboard-block-update / +dashboard-block-delete` |
Loading
Loading