-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat(base): support button field workflow binding #2264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package base | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/larksuite/cli/shortcuts/common" | ||
| ) | ||
|
|
||
| var BaseFieldButtonBind = common.Shortcut{ | ||
| Service: "base", | ||
| Command: "+field-button-bind", | ||
| Description: "Bind or rebind a button field to a workflow", | ||
| Risk: "write", | ||
| Scopes: []string{"base:field:update", "base:workflow:update"}, | ||
| AuthTypes: authTypes(), | ||
| Flags: []common.Flag{ | ||
| baseTokenFlag(true), | ||
| tableRefFlag(true), | ||
| fieldRefFlag(true), | ||
| {Name: "workflow-id", Desc: "workflow ID returned by +workflow-create or +workflow-list (wkf... prefix)", Required: true}, | ||
| }, | ||
| Tips: []string{ | ||
| `Example: lark-cli base +field-button-bind --base-token <base_token> --table-id <table_id> --field-id <field_id> --workflow-id <wkf_id>`, | ||
| "Create the workflow first with +workflow-create; new workflows are disabled until +workflow-enable is called.", | ||
| "Create the button field without a workflow ID in its property, then bind it with this command.", | ||
| "workflow-id must be the wkf... OpenAPI ID. Internal numeric workflow IDs are not accepted or displayed.", | ||
| "Binding is the source of truth; do not use property.trigger.config.id to create or update a binding.", | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return validateWorkflowIDFlag(runtime) | ||
| }, | ||
| DryRun: dryRunFieldButtonBind, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return executeFieldButtonBind(runtime) | ||
| }, | ||
| } |
| 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 BaseFieldButtonBindingGet = common.Shortcut{ | ||
| Service: "base", | ||
| Command: "+field-button-binding-get", | ||
| Description: "Get the workflow binding for a button field", | ||
| Risk: "read", | ||
| Scopes: []string{"base:field:read", "base:workflow:read"}, | ||
| AuthTypes: authTypes(), | ||
| Flags: []common.Flag{baseTokenFlag(true), tableRefFlag(true), fieldRefFlag(true)}, | ||
| Tips: []string{ | ||
| `Example: lark-cli base +field-button-binding-get --base-token <base_token> --table-id <table_id> --field-id <field_id>`, | ||
| "Returns the workflow_id as a wkf... OpenAPI ID when a binding exists.", | ||
| "The binding source of truth is the button workflow relation, not field property.trigger.", | ||
| }, | ||
| DryRun: dryRunFieldButtonBindingGet, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return executeFieldButtonBindingGet(runtime) | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,106 @@ | ||||||||||||||||||||||||||||||||||||||||||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||||||||||||||||||||||||||||||||||||||||||
| // SPDX-License-Identifier: MIT | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| package base | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| "github.com/larksuite/cli/shortcuts/common" | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func validateWorkflowIDFlag(runtime *common.RuntimeContext) error { | ||||||||||||||||||||||||||||||||||||||||||
| workflowID := strings.TrimSpace(runtime.Str("workflow-id")) | ||||||||||||||||||||||||||||||||||||||||||
| if workflowID == "" { | ||||||||||||||||||||||||||||||||||||||||||
| return baseFlagErrorf("--workflow-id must not be blank") | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if !strings.HasPrefix(workflowID, "wkf") { | ||||||||||||||||||||||||||||||||||||||||||
| return baseFlagErrorf("--workflow-id must be an OpenAPI workflow ID with wkf prefix; internal numeric workflow IDs are not accepted") | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject surrounding whitespace before API execution.
Reject leading or trailing whitespace so validation and execution use the same value. Proposed fix func validateWorkflowIDFlag(runtime *common.RuntimeContext) error {
- workflowID := strings.TrimSpace(runtime.Str("workflow-id"))
+ rawWorkflowID := runtime.Str("workflow-id")
+ workflowID := strings.TrimSpace(rawWorkflowID)
if workflowID == "" {
return baseFlagErrorf("--workflow-id must not be blank")
}
+ if workflowID != rawWorkflowID {
+ return baseFlagErrorf("--workflow-id must not contain leading or trailing whitespace")
+ }
if !strings.HasPrefix(workflowID, "wkf") {As per coding guidelines, preserve input values faithfully and return a typed validation error when the requested input cannot be honored. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func dryRunFieldButtonBind(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||||||||||||||||||||||||||||||||||||||||||
| return common.NewDryRunAPI(). | ||||||||||||||||||||||||||||||||||||||||||
| POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button-workflow:bind"). | ||||||||||||||||||||||||||||||||||||||||||
| Body(map[string]interface{}{"workflow_id": runtime.Str("workflow-id")}). | ||||||||||||||||||||||||||||||||||||||||||
| Set("base_token", runtime.Str("base-token")). | ||||||||||||||||||||||||||||||||||||||||||
| Set("table_id", baseTableID(runtime)). | ||||||||||||||||||||||||||||||||||||||||||
| Set("field_id", runtime.Str("field-id")) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func dryRunFieldButtonBindingGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||||||||||||||||||||||||||||||||||||||||||
| return common.NewDryRunAPI(). | ||||||||||||||||||||||||||||||||||||||||||
| GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button-workflow"). | ||||||||||||||||||||||||||||||||||||||||||
| Set("base_token", runtime.Str("base-token")). | ||||||||||||||||||||||||||||||||||||||||||
| Set("table_id", baseTableID(runtime)). | ||||||||||||||||||||||||||||||||||||||||||
| Set("field_id", runtime.Str("field-id")) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func dryRunFieldButtonUnbind(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||||||||||||||||||||||||||||||||||||||||||
| return common.NewDryRunAPI(). | ||||||||||||||||||||||||||||||||||||||||||
| POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button-workflow:unbind"). | ||||||||||||||||||||||||||||||||||||||||||
| Set("base_token", runtime.Str("base-token")). | ||||||||||||||||||||||||||||||||||||||||||
| Set("table_id", baseTableID(runtime)). | ||||||||||||||||||||||||||||||||||||||||||
| Set("field_id", runtime.Str("field-id")) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func dryRunWorkflowButtonFields(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||||||||||||||||||||||||||||||||||||||||||
| return common.NewDryRunAPI(). | ||||||||||||||||||||||||||||||||||||||||||
| GET("/open-apis/base/v3/bases/:base_token/workflows/:workflow_id/button-fields"). | ||||||||||||||||||||||||||||||||||||||||||
| Set("base_token", runtime.Str("base-token")). | ||||||||||||||||||||||||||||||||||||||||||
| Set("workflow_id", runtime.Str("workflow-id")) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func executeFieldButtonBind(runtime *common.RuntimeContext) error { | ||||||||||||||||||||||||||||||||||||||||||
| data, err := baseV3Call(runtime, "POST", | ||||||||||||||||||||||||||||||||||||||||||
| baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "fields", runtime.Str("field-id"), "button-workflow:bind"), | ||||||||||||||||||||||||||||||||||||||||||
| nil, | ||||||||||||||||||||||||||||||||||||||||||
| map[string]interface{}{"workflow_id": runtime.Str("workflow-id")}, | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| runtime.Out(data, nil) | ||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func executeFieldButtonBindingGet(runtime *common.RuntimeContext) error { | ||||||||||||||||||||||||||||||||||||||||||
| data, err := baseV3Call(runtime, "GET", | ||||||||||||||||||||||||||||||||||||||||||
| baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "fields", runtime.Str("field-id"), "button-workflow"), | ||||||||||||||||||||||||||||||||||||||||||
| nil, | ||||||||||||||||||||||||||||||||||||||||||
| nil, | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| runtime.Out(data, nil) | ||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func executeFieldButtonUnbind(runtime *common.RuntimeContext) error { | ||||||||||||||||||||||||||||||||||||||||||
| data, err := baseV3Call(runtime, "POST", | ||||||||||||||||||||||||||||||||||||||||||
| baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "fields", runtime.Str("field-id"), "button-workflow:unbind"), | ||||||||||||||||||||||||||||||||||||||||||
| nil, | ||||||||||||||||||||||||||||||||||||||||||
| nil, | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| runtime.Out(data, nil) | ||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func executeWorkflowButtonFields(runtime *common.RuntimeContext) error { | ||||||||||||||||||||||||||||||||||||||||||
| data, err := baseV3Call(runtime, "GET", | ||||||||||||||||||||||||||||||||||||||||||
| baseV3Path("bases", runtime.Str("base-token"), "workflows", runtime.Str("workflow-id"), "button-fields"), | ||||||||||||||||||||||||||||||||||||||||||
| nil, | ||||||||||||||||||||||||||||||||||||||||||
| nil, | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| runtime.Out(data, nil) | ||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| 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 BaseFieldButtonUnbind = common.Shortcut{ | ||
| Service: "base", | ||
| Command: "+field-button-unbind", | ||
| Description: "Unbind a button field from its workflow", | ||
| Risk: "write", | ||
| Scopes: []string{"base:field:update", "base:workflow:update"}, | ||
| AuthTypes: authTypes(), | ||
| Flags: []common.Flag{baseTokenFlag(true), tableRefFlag(true), fieldRefFlag(true)}, | ||
| Tips: []string{ | ||
| `Example: lark-cli base +field-button-unbind --base-token <base_token> --table-id <table_id> --field-id <field_id>`, | ||
| "Unbind is idempotent; repeating it should still return success when the server has no binding to delete.", | ||
| "Unbinding does not delete the field and does not disable or delete the workflow.", | ||
| }, | ||
| DryRun: dryRunFieldButtonUnbind, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return executeFieldButtonUnbind(runtime) | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package base | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/larksuite/cli/shortcuts/common" | ||
| ) | ||
|
|
||
| var BaseWorkflowButtonFields = common.Shortcut{ | ||
| Service: "base", | ||
| Command: "+workflow-button-fields", | ||
| Description: "List button fields bound to a workflow", | ||
| Risk: "read", | ||
| Scopes: []string{"base:field:read", "base:workflow:read"}, | ||
| AuthTypes: authTypes(), | ||
| Flags: []common.Flag{ | ||
| {Name: "base-token", Desc: "base token", Required: true}, | ||
| {Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true}, | ||
| }, | ||
| Tips: []string{ | ||
| `Example: lark-cli base +workflow-button-fields --base-token <base_token> --workflow-id <wkf_id>`, | ||
| "workflow-id must be a wkf... OpenAPI ID; internal numeric workflow IDs are not accepted or displayed.", | ||
| "Returns table_id and field_id pairs bound through the button workflow relation.", | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return validateWorkflowIDFlag(runtime) | ||
| }, | ||
| DryRun: dryRunWorkflowButtonFields, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| return executeWorkflowButtonFields(runtime) | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,12 +66,14 @@ metadata: | |
| | 一次性聚合统计 | `+data-query` | 必读 [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md) 和入口 [lark-base-data-query-guide.md](references/lark-base-data-query-guide.md);完整 DSL 再读 [lark-base-data-query.md](references/lark-base-data-query.md) | | ||
| | 公式字段 | `+field-create/update --json '{"type":"formula",...}'` | 必读 [formula-field-guide.md](references/formula-field-guide.md),读后再加隐藏确认 flag `--i-have-read-guide` | | ||
| | Lookup 字段 | `+field-create/update --json '{"type":"lookup",...}'` | 必读 [lookup-field-guide.md](references/lookup-field-guide.md),读后再加隐藏确认 flag `--i-have-read-guide` | | ||
| | 按钮字段 | `+workflow-create` → `+field-create --json '{"type":"button",...}'` → `+field-button-bind` | 先创建 disabled 的 ButtonTrigger Workflow,再创建不含 Workflow ID 的按钮字段,然后用 `+field-button-bind` 绑定;用 `+field-button-binding-get` / `+workflow-button-fields` 查询确认,最后按需 `+workflow-enable` | | ||
| | 表单提交 | `+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` | Base 内表单按 table 管理;先确定并复用真实 `table_id`。读 [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);题目显隐条件 `visible_rule` 结构见公共协议 [lark-base-filter-condition.md](references/lark-base-filter-condition.md) | | ||
| | Base 内表单管理 | `+form-list/get/create/update/delete` / `+form-questions-list/delete` | 缺少或不确定归属时,先用 `+table-list` 或 `+base-block-list` 取得真实 `table_id`;这些命令使用 `--base-token + --table-id` 并在整个工作流中复用同一 `table_id`,删除前确认目标表单 | | ||
| | 分享表单详情 | `+form-detail --share-token <share_token>` | 只接受表单分享链接里的 `share_token`,不要传 `--base-token` / `--form-id`;提交前读 [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`;读取完整仪表盘时按 block 类型分流,文本和不支持直接取数的图表按 reference 恢复 | | ||
| | 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 与启停状态 | | ||
| | 按钮字段绑定 | `+field-button-bind` / `+field-button-binding-get` / `+field-button-unbind` / `+workflow-button-fields` | 只接受和展示 `wkf...` workflow ID;绑定事实来自独立按钮绑定接口,不来自字段 `property.trigger` | | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Clarify which identifiers use the This text can be read as saying that every shortcut accepts and displays only workflow IDs. The documented bind command also requires 🤖 Prompt for AI Agents |
||
| | 高级权限与角色 | `+advperm-*` / `+role-*` | 角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md);角色 create/update 或解读完整配置再读权限 JSON SSOT [role-config.md](references/role-config.md);系统角色不可删除;关闭高级权限会影响自定义角色 | | ||
|
|
||
| ## Base 心智模型 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Assert the typed validation contract.
This test checks only the
"wkf prefix"message. It will accept an untyped error with the same text.Use
errs.ProblemOfto assert the validation category and subtype. Useerrors.Aswith*errs.ValidationErrorto assertParam == "--workflow-id".As per coding guidelines, error-path tests must assert typed metadata instead of relying only on message substrings. Based on learnings, use
errors.Asbecauseerrs.ProblemOfdoes not exposeParam.🤖 Prompt for AI Agents
Sources: Coding guidelines, Learnings