diff --git a/shortcuts/sheets/sheet_dropdown.go b/shortcuts/sheets/sheet_dropdown.go new file mode 100644 index 00000000..fe092bba --- /dev/null +++ b/shortcuts/sheets/sheet_dropdown.go @@ -0,0 +1,333 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/larksuite/cli/internal/validate" + "github.com/larksuite/cli/shortcuts/common" +) + +func dataValidationBasePath(token string) string { + return fmt.Sprintf("/open-apis/sheets/v2/spreadsheets/%s/dataValidation", + validate.EncodePathSegment(token)) +} + +func dataValidationSheetPath(token, sheetID string) string { + return fmt.Sprintf("%s/%s", dataValidationBasePath(token), validate.EncodePathSegment(sheetID)) +} + +func validateDropdownToken(runtime *common.RuntimeContext) (string, error) { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + if token == "" { + return "", common.FlagErrorf("specify --url or --spreadsheet-token") + } + return token, nil +} + +func parseJSONStringArray(flagName, value string) ([]interface{}, error) { + var typed []string + if err := json.Unmarshal([]byte(value), &typed); err != nil { + return nil, common.FlagErrorf("--%s must be a JSON array of strings: %v", flagName, err) + } + if typed == nil { + return nil, common.FlagErrorf("--%s must be a JSON array, got null", flagName) + } + arr := make([]interface{}, len(typed)) + for i, s := range typed { + arr[i] = s + } + return arr, nil +} + +func validateRangesFlag(runtime *common.RuntimeContext) ([]interface{}, error) { + ranges, err := parseJSONStringArray("ranges", runtime.Str("ranges")) + if err != nil { + return nil, err + } + if len(ranges) == 0 { + return nil, common.FlagErrorf("--ranges must not be empty") + } + for i, r := range ranges { + s, _ := r.(string) + if _, _, ok := splitSheetRange(s); !ok { + return nil, common.FlagErrorf("--ranges[%d] %q must be a fully qualified range with sheet ID prefix (e.g. !A2:A100)", i, s) + } + } + return ranges, nil +} + +func buildDropdownBody(runtime *common.RuntimeContext) (map[string]interface{}, error) { + condValues, err := parseJSONStringArray("condition-values", runtime.Str("condition-values")) + if err != nil { + return nil, err + } + if len(condValues) == 0 { + return nil, common.FlagErrorf("--condition-values must not be empty") + } + + dv := map[string]interface{}{ + "conditionValues": condValues, + } + + opts := map[string]interface{}{} + if runtime.Cmd.Flags().Changed("multiple") { + opts["multipleValues"] = runtime.Bool("multiple") + } + if runtime.Cmd.Flags().Changed("highlight") { + opts["highlightValidData"] = runtime.Bool("highlight") + } + if runtime.Str("colors") != "" { + colors, err := parseJSONStringArray("colors", runtime.Str("colors")) + if err != nil { + return nil, err + } + if len(colors) != len(condValues) { + return nil, common.FlagErrorf("--colors length (%d) must match --condition-values length (%d)", len(colors), len(condValues)) + } + opts["colors"] = colors + } + if len(opts) > 0 { + dv["options"] = opts + } + + return dv, nil +} + +// SheetSetDropdown sets dropdown list validation on a range. +var SheetSetDropdown = common.Shortcut{ + Service: "sheets", + Command: "+set-dropdown", + Description: "Set dropdown list on a cell range", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token"}, + {Name: "range", Desc: "cell range (!A2:A100)", Required: true}, + {Name: "condition-values", Desc: `dropdown options as JSON array (e.g. '["opt1","opt2"]'), max 500, each <=100 chars, no commas`, Required: true}, + {Name: "multiple", Desc: "enable multi-select (default false)", Type: "bool"}, + {Name: "highlight", Desc: "color-code options (default false)", Type: "bool"}, + {Name: "colors", Desc: `RGB hex color array (e.g. '["#1FB6C1","#F006C2"]'), must match condition-values length`}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := validateDropdownToken(runtime); err != nil { + return err + } + if _, _, ok := splitSheetRange(runtime.Str("range")); !ok { + return common.FlagErrorf("--range must be a fully qualified range with sheet ID prefix (e.g. !A2:A100)") + } + _, err := buildDropdownBody(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateDropdownToken(runtime) + dv, _ := buildDropdownBody(runtime) + return common.NewDryRunAPI(). + POST("/open-apis/sheets/v2/spreadsheets/:token/dataValidation"). + Body(map[string]interface{}{ + "range": runtime.Str("range"), + "dataValidationType": "list", + "dataValidation": dv, + }). + Set("token", token) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateDropdownToken(runtime) + dv, err := buildDropdownBody(runtime) + if err != nil { + return err + } + + data, err := runtime.CallAPI("POST", dataValidationBasePath(token), nil, + map[string]interface{}{ + "range": runtime.Str("range"), + "dataValidationType": "list", + "dataValidation": dv, + }, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +// SheetUpdateDropdown updates dropdown list settings for given ranges. +var SheetUpdateDropdown = common.Shortcut{ + Service: "sheets", + Command: "+update-dropdown", + Description: "Update dropdown list settings", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "ranges", Desc: `ranges as JSON array (e.g. '["sheetId!A1:A100"]')`, Required: true}, + {Name: "condition-values", Desc: `dropdown options as JSON array (e.g. '["opt1","opt2"]')`, Required: true}, + {Name: "multiple", Desc: "enable multi-select (default false)", Type: "bool"}, + {Name: "highlight", Desc: "color-code options (default false)", Type: "bool"}, + {Name: "colors", Desc: `RGB hex color array, must match condition-values length`}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := validateDropdownToken(runtime); err != nil { + return err + } + if _, err := validateRangesFlag(runtime); err != nil { + return err + } + _, err := buildDropdownBody(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateDropdownToken(runtime) + ranges, _ := parseJSONStringArray("ranges", runtime.Str("ranges")) + dv, _ := buildDropdownBody(runtime) + return common.NewDryRunAPI(). + PUT("/open-apis/sheets/v2/spreadsheets/:token/dataValidation/:sheet_id"). + Body(map[string]interface{}{ + "ranges": ranges, + "dataValidationType": "list", + "dataValidation": dv, + }). + Set("token", token).Set("sheet_id", runtime.Str("sheet-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateDropdownToken(runtime) + ranges, err := parseJSONStringArray("ranges", runtime.Str("ranges")) + if err != nil { + return err + } + dv, err := buildDropdownBody(runtime) + if err != nil { + return err + } + + data, err := runtime.CallAPI("PUT", dataValidationSheetPath(token, runtime.Str("sheet-id")), nil, + map[string]interface{}{ + "ranges": ranges, + "dataValidationType": "list", + "dataValidation": dv, + }, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +// SheetGetDropdown queries dropdown list settings for a range. +var SheetGetDropdown = common.Shortcut{ + Service: "sheets", + Command: "+get-dropdown", + Description: "Get dropdown list settings for a range", + Risk: "read", + Scopes: []string{"sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token"}, + {Name: "range", Desc: "cell range (!A2:A100)", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := validateDropdownToken(runtime); err != nil { + return err + } + if _, _, ok := splitSheetRange(runtime.Str("range")); !ok { + return common.FlagErrorf("--range must be a fully qualified range with sheet ID prefix (e.g. !A2:A100)") + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateDropdownToken(runtime) + return common.NewDryRunAPI(). + GET("/open-apis/sheets/v2/spreadsheets/:token/dataValidation?range=:range&dataValidationType=list"). + Set("token", token).Set("range", runtime.Str("range")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateDropdownToken(runtime) + data, err := runtime.CallAPI("GET", dataValidationBasePath(token), + map[string]interface{}{ + "range": runtime.Str("range"), + "dataValidationType": "list", + }, nil, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +// SheetDeleteDropdown deletes dropdown list settings from given ranges. +var SheetDeleteDropdown = common.Shortcut{ + Service: "sheets", + Command: "+delete-dropdown", + Description: "Delete dropdown list from cell ranges", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token"}, + {Name: "ranges", Desc: `ranges as JSON array (e.g. '["sheetId!A2:A100"]'), max 100 ranges`, Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := validateDropdownToken(runtime); err != nil { + return err + } + _, err := validateRangesFlag(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateDropdownToken(runtime) + ranges, _ := parseJSONStringArray("ranges", runtime.Str("ranges")) + dvRanges := make([]interface{}, 0, len(ranges)) + for _, r := range ranges { + dvRanges = append(dvRanges, map[string]interface{}{"range": r}) + } + return common.NewDryRunAPI(). + DELETE("/open-apis/sheets/v2/spreadsheets/:token/dataValidation"). + Body(map[string]interface{}{ + "dataValidationRanges": dvRanges, + }). + Set("token", token) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateDropdownToken(runtime) + ranges, err := parseJSONStringArray("ranges", runtime.Str("ranges")) + if err != nil { + return err + } + + dvRanges := make([]interface{}, 0, len(ranges)) + for _, r := range ranges { + dvRanges = append(dvRanges, map[string]interface{}{"range": r}) + } + + data, err := runtime.CallAPI("DELETE", dataValidationBasePath(token), nil, + map[string]interface{}{ + "dataValidationRanges": dvRanges, + }, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} diff --git a/shortcuts/sheets/sheet_dropdown_test.go b/shortcuts/sheets/sheet_dropdown_test.go new file mode 100644 index 00000000..135d91ce --- /dev/null +++ b/shortcuts/sheets/sheet_dropdown_test.go @@ -0,0 +1,552 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "bytes" + "context" + "encoding/json" + "strings" + "testing" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/internal/httpmock" +) + +// ── SetDropdown ───────────────────────────────────────────────────────────── + +func TestSetDropdownValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", + "range": "s1!A2:A100", "condition-values": `["opt1","opt2"]`, + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + err := SheetSetDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestSetDropdownValidateInvalidConditionValues(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", + "range": "s1!A2:A100", "condition-values": "not-json", + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + err := SheetSetDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--condition-values must be a JSON array") { + t.Fatalf("expected JSON array error, got: %v", err) + } +} + +func TestSetDropdownValidateNonStringConditionValues(t *testing.T) { + t.Parallel() + cases := []struct { + name string + input string + }{ + {"mixed types", `["ok", 1, null]`}, + {"all numbers", `[1, 2, 3]`}, + {"null literal", `null`}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", + "range": "s1!A2:A100", "condition-values": tc.input, + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + err := SheetSetDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--condition-values must be") { + t.Fatalf("expected validation error for %q, got: %v", tc.input, err) + } + }) + } +} + +func TestSetDropdownValidateInvalidColors(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", + "range": "s1!A2:A100", "condition-values": `["opt1","opt2"]`, + "colors": "bad-json", + }, map[string]bool{"multiple": false, "highlight": true}) + err := SheetSetDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--colors must be a JSON array") { + t.Fatalf("expected colors JSON error, got: %v", err) + } +} + +func TestSetDropdownValidateRangeMissingSheetID(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", + "range": "A2:A100", "condition-values": `["opt1"]`, + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + err := SheetSetDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "fully qualified range") { + t.Fatalf("expected range validation error, got: %v", err) + } +} + +func TestSetDropdownValidateEmptyConditionValues(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", + "range": "s1!A2:A100", "condition-values": `[]`, + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + err := SheetSetDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--condition-values must not be empty") { + t.Fatalf("expected empty error, got: %v", err) + } +} + +func TestSetDropdownValidateColorsMismatchLength(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", + "range": "s1!A2:A100", "condition-values": `["a","b","c"]`, + "colors": `["#FF0000"]`, + }, map[string]bool{"multiple": false, "highlight": true}) + err := SheetSetDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--colors length") { + t.Fatalf("expected length mismatch error, got: %v", err) + } +} + +func TestSetDropdownValidateSuccess(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", + "range": "s1!A2:A100", "condition-values": `["opt1","opt2"]`, + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + if err := SheetSetDropdown.Validate(context.Background(), rt); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSetDropdownDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", + "range": "s1!A2:A100", "condition-values": `["opt1","opt2"]`, + "colors": "", + }, map[string]bool{"multiple": true, "highlight": false}) + got := mustMarshalSheetsDryRun(t, SheetSetDropdown.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"POST"`) { + t.Fatalf("DryRun should use POST: %s", got) + } + if !strings.Contains(got, `dataValidation`) { + t.Fatalf("DryRun missing dataValidation: %s", got) + } + if !strings.Contains(got, `"dataValidationType":"list"`) { + t.Fatalf("DryRun missing dataValidationType: %s", got) + } +} + +func TestSetDropdownExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/dataValidation", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{}}, + }) + err := mountAndRunSheets(t, SheetSetDropdown, []string{ + "+set-dropdown", "--spreadsheet-token", "shtTOKEN", + "--range", "s1!A2:A100", "--condition-values", `["opt1","opt2","opt3"]`, + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSetDropdownExecuteWithMultipleAndColors(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + stub := &httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/dataValidation", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{}}, + } + reg.Register(stub) + err := mountAndRunSheets(t, SheetSetDropdown, []string{ + "+set-dropdown", "--spreadsheet-token", "shtTOKEN", + "--range", "s1!A2:A100", "--condition-values", `["a","b"]`, + "--multiple", "--highlight", "--colors", `["#1FB6C1","#F006C2"]`, + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + var body map[string]interface{} + if err := json.Unmarshal(stub.CapturedBody, &body); err != nil { + t.Fatalf("parse body: %v", err) + } + dv, _ := body["dataValidation"].(map[string]interface{}) + opts, _ := dv["options"].(map[string]interface{}) + if opts["multipleValues"] != true { + t.Fatalf("expected multipleValues=true, got: %v", opts["multipleValues"]) + } + if opts["highlightValidData"] != true { + t.Fatalf("expected highlightValidData=true, got: %v", opts["highlightValidData"]) + } +} + +func TestSetDropdownExecuteAPIError(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/dataValidation", + Status: 400, Body: map[string]interface{}{"code": 90001, "msg": "invalid"}, + }) + err := mountAndRunSheets(t, SheetSetDropdown, []string{ + "+set-dropdown", "--spreadsheet-token", "shtTOKEN", + "--range", "s1!A2:A100", "--condition-values", `["opt1"]`, + "--as", "user", + }, f, nil) + if err == nil { + t.Fatal("expected error") + } +} + +func TestSetDropdownWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v2/spreadsheets/shtFromURL/dataValidation", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{}}, + }) + err := mountAndRunSheets(t, SheetSetDropdown, []string{ + "+set-dropdown", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--range", "s1!A2:A100", "--condition-values", `["opt1"]`, + "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// ── UpdateDropdown ────────────────────────────────────────────────────────── + +func TestUpdateDropdownValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", "sheet-id": "s1", + "ranges": `["s1!A1:A100"]`, "condition-values": `["opt1"]`, + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + err := SheetUpdateDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestUpdateDropdownValidateInvalidRanges(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "sheet-id": "s1", + "ranges": "not-json", "condition-values": `["opt1"]`, + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + err := SheetUpdateDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--ranges must be a JSON array") { + t.Fatalf("expected JSON array error, got: %v", err) + } +} + +func TestUpdateDropdownValidateRangesMissingSheetID(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "sheet-id": "s1", + "ranges": `["A1:A100"]`, "condition-values": `["opt1"]`, + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + err := SheetUpdateDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "fully qualified range") { + t.Fatalf("expected range validation error, got: %v", err) + } +} + +func TestUpdateDropdownValidateEmptyRanges(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "sheet-id": "s1", + "ranges": `[]`, "condition-values": `["opt1"]`, + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + err := SheetUpdateDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--ranges must not be empty") { + t.Fatalf("expected empty error, got: %v", err) + } +} + +func TestUpdateDropdownValidateInvalidColors(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "sheet-id": "s1", + "ranges": `["s1!A1:A100"]`, "condition-values": `["opt1"]`, + "colors": "{not-array}", + }, map[string]bool{"multiple": false, "highlight": true}) + err := SheetUpdateDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--colors must be a JSON array") { + t.Fatalf("expected colors JSON error, got: %v", err) + } +} + +func TestUpdateDropdownDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", + "ranges": `["sheet1!A1:A100"]`, "condition-values": `["new1","new2"]`, + "colors": "", + }, map[string]bool{"multiple": false, "highlight": false}) + got := mustMarshalSheetsDryRun(t, SheetUpdateDropdown.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"PUT"`) { + t.Fatalf("DryRun should use PUT: %s", got) + } + if !strings.Contains(got, `sheet1`) { + t.Fatalf("DryRun missing sheet_id: %s", got) + } +} + +func TestUpdateDropdownExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "PUT", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/dataValidation/sheet1", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "spreadsheetToken": "shtTOKEN", "sheetId": "sheet1", + }}, + }) + err := mountAndRunSheets(t, SheetUpdateDropdown, []string{ + "+update-dropdown", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--ranges", `["sheet1!A1:A100"]`, + "--condition-values", `["new1","new2"]`, "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestUpdateDropdownWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "PUT", URL: "/open-apis/sheets/v2/spreadsheets/shtFromURL/dataValidation/sheet1", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{}}, + }) + err := mountAndRunSheets(t, SheetUpdateDropdown, []string{ + "+update-dropdown", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--ranges", `["sheet1!A1:A100"]`, + "--condition-values", `["opt1"]`, "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// ── GetDropdown ───────────────────────────────────────────────────────────── + +func TestGetDropdownValidateRangeMissingSheetID(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "range": "A2:A100", + }, nil) + err := SheetGetDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "fully qualified range") { + t.Fatalf("expected range validation error, got: %v", err) + } +} + +func TestGetDropdownValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", "range": "s1!A2:A100", + }, nil) + err := SheetGetDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestGetDropdownDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "range": "s1!A2:A100", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetGetDropdown.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"GET"`) { + t.Fatalf("DryRun should use GET: %s", got) + } + if !strings.Contains(got, `dataValidation`) { + t.Fatalf("DryRun missing dataValidation path: %s", got) + } +} + +func TestGetDropdownExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/dataValidation", + Body: map[string]interface{}{"code": 0, "msg": "Success", "data": map[string]interface{}{ + "dataValidations": []interface{}{ + map[string]interface{}{ + "dataValidationType": "list", + "conditionValues": []interface{}{"opt1", "opt2"}, + "ranges": []interface{}{"s1!A2:A100"}, + }, + }, + }}, + }) + err := mountAndRunSheets(t, SheetGetDropdown, []string{ + "+get-dropdown", "--spreadsheet-token", "shtTOKEN", + "--range", "s1!A2:A100", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "dataValidations") { + t.Fatalf("stdout missing dataValidations: %s", stdout.String()) + } +} + +func TestGetDropdownWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", URL: "/open-apis/sheets/v2/spreadsheets/shtFromURL/dataValidation", + Body: map[string]interface{}{"code": 0, "msg": "Success", "data": map[string]interface{}{ + "dataValidations": []interface{}{}, + }}, + }) + err := mountAndRunSheets(t, SheetGetDropdown, []string{ + "+get-dropdown", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--range", "s1!A2:A100", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// ── DeleteDropdown ────────────────────────────────────────────────────────── + +func TestDeleteDropdownValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", "ranges": `["s1!A2:A100"]`, + }, nil) + err := SheetDeleteDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestDeleteDropdownValidateRangesMissingSheetID(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "ranges": `["B1:B50"]`, + }, nil) + err := SheetDeleteDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "fully qualified range") { + t.Fatalf("expected range validation error, got: %v", err) + } +} + +func TestDeleteDropdownValidateEmptyRanges(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "ranges": `[]`, + }, nil) + err := SheetDeleteDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--ranges must not be empty") { + t.Fatalf("expected empty error, got: %v", err) + } +} + +func TestDeleteDropdownValidateInvalidRanges(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "ranges": "bad", + }, nil) + err := SheetDeleteDropdown.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--ranges must be a JSON array") { + t.Fatalf("expected JSON array error, got: %v", err) + } +} + +func TestDeleteDropdownDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "ranges": `["s1!A2:A100","s1!C1:C50"]`, + }, nil) + got := mustMarshalSheetsDryRun(t, SheetDeleteDropdown.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"DELETE"`) { + t.Fatalf("DryRun should use DELETE: %s", got) + } + if !strings.Contains(got, `dataValidationRanges`) { + t.Fatalf("DryRun missing dataValidationRanges: %s", got) + } +} + +func TestDeleteDropdownExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "DELETE", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/dataValidation", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "rangeResults": []interface{}{ + map[string]interface{}{"range": "s1!A2:A100", "success": true, "updatedCells": 99}, + }, + }}, + }) + err := mountAndRunSheets(t, SheetDeleteDropdown, []string{ + "+delete-dropdown", "--spreadsheet-token", "shtTOKEN", + "--ranges", `["s1!A2:A100"]`, "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "rangeResults") { + t.Fatalf("stdout missing rangeResults: %s", stdout.String()) + } +} + +func TestDeleteDropdownExecuteMultipleRanges(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + stub := &httpmock.Stub{ + Method: "DELETE", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/dataValidation", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{}}, + } + reg.Register(stub) + err := mountAndRunSheets(t, SheetDeleteDropdown, []string{ + "+delete-dropdown", "--spreadsheet-token", "shtTOKEN", + "--ranges", `["s1!A2:A100","s1!C1:C50"]`, "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + var body map[string]interface{} + if err := json.Unmarshal(stub.CapturedBody, &body); err != nil { + t.Fatalf("parse body: %v", err) + } + dvRanges, _ := body["dataValidationRanges"].([]interface{}) + if len(dvRanges) != 2 { + t.Fatalf("expected 2 ranges, got: %d", len(dvRanges)) + } +} + +func TestDeleteDropdownWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "DELETE", URL: "/open-apis/sheets/v2/spreadsheets/shtFromURL/dataValidation", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{}}, + }) + err := mountAndRunSheets(t, SheetDeleteDropdown, []string{ + "+delete-dropdown", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--ranges", `["s1!A2:A100"]`, "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// suppress unused import for bytes in case the test helpers already import it +var _ = (*bytes.Buffer)(nil) diff --git a/shortcuts/sheets/shortcuts.go b/shortcuts/sheets/shortcuts.go index 65bffb3d..b97df66c 100644 --- a/shortcuts/sheets/shortcuts.go +++ b/shortcuts/sheets/shortcuts.go @@ -36,5 +36,9 @@ func Shortcuts() []common.Shortcut { SheetListFilterViewConditions, SheetGetFilterViewCondition, SheetDeleteFilterViewCondition, + SheetSetDropdown, + SheetUpdateDropdown, + SheetGetDropdown, + SheetDeleteDropdown, } } diff --git a/skill-template/domains/sheets.md b/skill-template/domains/sheets.md index de52d837..53dea9fa 100644 --- a/skill-template/domains/sheets.md +++ b/skill-template/domains/sheets.md @@ -157,7 +157,9 @@ lark-cli sheets +write --url "URL" --sheet-id "sheetId" --range "C6" \ --values '[["=SUM(C2:C5)"]]' ``` +> **公式语法参考**:涉及 ARRAYFORMULA、原生数组函数、MAP/LAMBDA、日期差、Excel 公式改写等飞书特有规则时,先阅读 [`references/lark-sheets-formula.md`](references/lark-sheets-formula.md)。 + **限制**: -- 公式不支持跨表引用(IMPORTRANGE) +- 公式支持 IMPORTRANGE 跨表引用(最多 5 层嵌套、每个工作表最多 100 个引用) - @人仅支持同租户用户,单次最多 50 人 -- 下拉列表需先调用设置下拉列表接口,值中的字符串不能包含逗号 +- 下拉列表需**先通过 `+set-dropdown` 配置下拉选项**,否则 `multipleValue` 写入会变成纯文本。值中的字符串不能包含逗号 diff --git a/skills/lark-sheets/SKILL.md b/skills/lark-sheets/SKILL.md index 6cda35ee..0e4809dc 100644 --- a/skills/lark-sheets/SKILL.md +++ b/skills/lark-sheets/SKILL.md @@ -170,10 +170,12 @@ lark-cli sheets +write --url "URL" --sheet-id "sheetId" --range "C6" \ --values '[["=SUM(C2:C5)"]]' ``` +> **公式语法参考**:涉及 ARRAYFORMULA、原生数组函数、MAP/LAMBDA、日期差、Excel 公式改写等飞书特有规则时,先阅读 [`references/lark-sheets-formula.md`](references/lark-sheets-formula.md)。 + **限制**: -- 公式不支持跨表引用(IMPORTRANGE) +- 公式支持 IMPORTRANGE 跨表引用(最多 5 层嵌套、每个工作表最多 100 个引用) - @人仅支持同租户用户,单次最多 50 人 -- 下拉列表需先调用设置下拉列表接口,值中的字符串不能包含逗号 +- 下拉列表需**先配置下拉选项**,否则 `multipleValue` 写入会变成纯文本。配置方法见 [`references/lark-sheets-set-dropdown.md`](references/lark-sheets-set-dropdown.md)。值中的字符串不能包含逗号 ## Shortcuts(推荐优先使用) @@ -210,6 +212,15 @@ Shortcut 是对常用操作的高级封装(`lark-cli sheets + [flags]` | [`+get-filter-view-condition`](references/lark-sheets-get-filter-view-condition.md) | Get a filter condition by column | | [`+delete-filter-view-condition`](references/lark-sheets-delete-filter-view-condition.md) | Delete a filter condition | +### 下拉列表 + +| Shortcut | 说明 | +|----------|------| +| [`+set-dropdown`](references/lark-sheets-set-dropdown.md) | 设置下拉列表(`multipleValue` 写入的前置步骤) | +| [`+update-dropdown`](references/lark-sheets-update-dropdown.md) | 更新下拉列表选项 | +| [`+get-dropdown`](references/lark-sheets-get-dropdown.md) | 查询下拉列表配置 | +| [`+delete-dropdown`](references/lark-sheets-delete-dropdown.md) | 删除下拉列表 | + ## API Resources ```bash diff --git a/skills/lark-sheets/references/lark-sheets-delete-dropdown.md b/skills/lark-sheets/references/lark-sheets-delete-dropdown.md new file mode 100644 index 00000000..93ea4340 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-delete-dropdown.md @@ -0,0 +1,46 @@ + +# sheets +delete-dropdown(删除下拉列表) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +delete-dropdown`。 + +删除指定范围的下拉列表配置。支持一次删除多个范围。 + +> [!CAUTION] +> 这是**删除操作** —— 执行前必须确认用户意图。可以先用 `--dry-run` 预览。 + +## 命令 + +```bash +# 删除单个范围 +lark-cli sheets +delete-dropdown --url "https://example.larksuite.com/sheets/shtxxxxxxxx" \ + --ranges '["!A2:A100"]' + +# 删除多个范围 +lark-cli sheets +delete-dropdown --spreadsheet-token "shtxxxxxxxx" \ + --ranges '["!A2:A100", "!C1:C50"]' +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token` | 否 | 表格 token | +| `--ranges` | 是 | 范围 JSON 数组(如 `'["sheetId!A2:A100"]'`),单个范围最多 5000 格,单次最多 100 个范围 | +| `--dry-run` | 否 | 仅打印参数,不执行请求 | + +## 输出 + +JSON,包含: + +- `rangeResults[].range` — 对应的范围 +- `rangeResults[].success` — 是否成功 +- `rangeResults[].updatedCells` — 影响的单元格数量 + +## 参考 + +- [lark-sheets-set-dropdown](lark-sheets-set-dropdown.md) — 设置下拉列表 +- [lark-sheets-update-dropdown](lark-sheets-update-dropdown.md) — 更新下拉列表 +- [lark-sheets-get-dropdown](lark-sheets-get-dropdown.md) — 查询下拉列表 diff --git a/skills/lark-sheets/references/lark-sheets-formula.md b/skills/lark-sheets/references/lark-sheets-formula.md new file mode 100644 index 00000000..48072a81 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-formula.md @@ -0,0 +1,89 @@ + +# 飞书表格公式规则 + +> 生成或改写飞书电子表格公式时的参考规则。飞书不像 Excel 365 默认 spill,普通公式对区域默认"投影"(只取当前行/列对应的单值),必须显式使用 `ARRAYFORMULA` 或原生数组函数才能逐项展开。 + +## 写入方式 + +公式必须使用对象格式写入(参见 SKILL.md「单元格数据类型」): + +```bash +--values '[[{"type":"formula","text":"=SUM(A1:A10)"}]]' +``` + +## ARRAYFORMULA 判断流程 + +1. 结果是**标量**(单值)→ 不需要 +2. 结果是**数组**,且公式中**有**原生数组函数 → 不需要(数组语义自动传播) +3. 结果是**数组**,且公式中**无**原生数组函数,对区域做标量计算 → 加 `ARRAYFORMULA` + +```text +# 有原生数组函数,无需包裹 +=FILTER(A2:A10,B2:B10="x")+1 ✓ +=XLOOKUP(E2:E10,A2:A10,B2:B10)*100 ✓ +=MAP(A2:A10,LAMBDA(x,x*2))-1 ✓ + +# 无原生数组函数,必须包裹 +=ARRAYFORMULA(A2:A100*B2:B100) ✓ +=ARRAYFORMULA(IF(A2:A100>0,B2:B100,""))✓ +``` + +## 原生数组函数清单(无需 ARRAYFORMULA) + +`ARRAYFORMULA` `ARRAY_CONSTRAIN` `BYCOL` `BYROW` `CELL` `CHOOSECOLS` `CHOOSEROWS` `DROP` `EXPAND` `FILTER` `FLATTEN` `FREQUENCY` `GROWTH` `HSTACK` `IMPORTDATA` `IMPORTFEED` `IMPORTHTML` `IMPORTRANGE` `IMPORTXML` `LINEST` `LOGEST` `LOOKUP` `MAKEARRAY` `MAP` `MINVERSE` `MMULT` `MUNIT` `QUERY` `RANDARRAY` `REDUCE` `REGEXEXTRACT` `SCAN` `SEQUENCE` `SORT` `SORTBY` `SORTN` `SPLIT` `SUMPRODUCT` `SWITCH` `TAKE` `TEXTSPLIT` `TOCOL` `TOROW` `TRANSPOSE` `TREND` `UNIQUE` `VSTACK` `WRAPCOLS` `WRAPROWS` `XLOOKUP` + +## 高风险函数:INDEX / OFFSET / ROW / COLUMN / MATCH + +行号/列号/偏移量本身是数组时,必须显式包裹: + +```text +=ARRAYFORMULA(INDEX(...)) +=ARRAYFORMULA(ROW(...)) +``` + +例外:结果直接交给聚合函数消费时不需要:`=SUM(INDEX(A1:B2,0,1))` ✓ + +## 隐式逐项求值 → MAP/LAMBDA + +Excel 中 `SUBTOTAL`、`INDIRECT`、`OFFSET` 等在 `SUMPRODUCT` 内会隐式逐行求值,飞书不会。用 MAP 显式遍历: + +```text +# Excel +=SUMPRODUCT(SUBTOTAL(103,INDIRECT("E"&ROW($E$16:$E$387)))) + +# 飞书 +=SUMPRODUCT(MAP(ARRAYFORMULA(ROW($E$16:$E$387)),LAMBDA(r,SUBTOTAL(103,INDIRECT("E"&r))))) +``` + +同类场景:`SUMIF/COUNTIF/SUMIFS` 的范围参数来自 `INDIRECT/OFFSET` 时也需要 MAP。 + +## 多维结果降维 + +飞书公式结果只能是二维,不能返回"区域的列表"。合并多个区域时: + +| 需求 | 写法 | +|------|------| +| 上下堆叠 | `=VSTACK(a, b, c)` | +| 左右拼接 | `=HSTACK(a, b, c)` | +| 压成单列 | `=TOCOL(...)` | +| 压成单行 | `=TOROW(...)` | +| 归约为标量 | `=REDUCE(init, arr, LAMBDA(acc, x, ...))` | + +## 日期差 + +| 需求 | 正确写法 | 错误写法 | +|------|---------|---------| +| 天数差 | `=DAYS(B2,A2)` 或 `=DATEDIF(A2,B2,"D")` 或 `=B2-A2` | `=DAY(B2-A2)` | +| 月份差 | `=DATEDIF(A2,B2,"M")` | `=MONTH(B2-A2)` | +| 年份差 | `=DATEDIF(A2,B2,"Y")` | `=YEAR(B2-A2)` | +| 工作日差 | `=NETWORKDAYS(A2,B2)` | — | + +## 飞书不支持的 Excel 语法 + +| Excel 语法 | 飞书替代 | +|-----------|---------| +| `=@A1:A10`(隐式交叉) | `=A1:A10`(飞书默认投影,去掉 `@`) | +| `=A1#`(spill range) | 改成明确范围,或用 `TAKE`/`DROP`/`ARRAY_CONSTRAIN` | +| `=SUM(Table1[Amount])`(结构化引用) | `=SUM(A2:A100)`(改为 A1 区域) | +| `{=A1:A10*B1:B10}`(CSE 花括号) | `=ARRAYFORMULA(A1:A10*B1:B10)` | +| `STOCKHISTORY` / `WEBSERVICE` / `CUBE*` | 飞书无等价函数 | diff --git a/skills/lark-sheets/references/lark-sheets-get-dropdown.md b/skills/lark-sheets/references/lark-sheets-get-dropdown.md new file mode 100644 index 00000000..ab7874e1 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-get-dropdown.md @@ -0,0 +1,43 @@ + +# sheets +get-dropdown(查询下拉列表) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +get-dropdown`。 + +查询指定范围内已配置的下拉列表设置,包括选项值、是否多选、颜色映射等。 + +## 命令 + +```bash +lark-cli sheets +get-dropdown --url "https://example.larksuite.com/sheets/shtxxxxxxxx" \ + --range "!A2:A100" + +lark-cli sheets +get-dropdown --spreadsheet-token "shtxxxxxxxx" \ + --range "!A2:A100" +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token` | 否 | 表格 token | +| `--range` | 是 | 范围(如 `!A2:A100`) | +| `--dry-run` | 否 | 仅打印参数,不执行请求 | + +## 输出 + +JSON,包含: + +- `dataValidations[].conditionValues` — 下拉选项列表 +- `dataValidations[].ranges` — 应用范围 +- `dataValidations[].options.multipleValues` — 是否多选 +- `dataValidations[].options.highlightValidData` — 是否着色 +- `dataValidations[].options.colorValueMap` — 选项颜色映射 + +## 参考 + +- [lark-sheets-set-dropdown](lark-sheets-set-dropdown.md) — 设置下拉列表 +- [lark-sheets-update-dropdown](lark-sheets-update-dropdown.md) — 更新下拉列表 +- [lark-sheets-delete-dropdown](lark-sheets-delete-dropdown.md) — 删除下拉列表 diff --git a/skills/lark-sheets/references/lark-sheets-set-dropdown.md b/skills/lark-sheets/references/lark-sheets-set-dropdown.md new file mode 100644 index 00000000..18ab7073 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-set-dropdown.md @@ -0,0 +1,62 @@ + +# sheets +set-dropdown(设置下拉列表) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +set-dropdown`。 + +为指定范围的单元格配置下拉列表选项。**这是使用 `multipleValue` 格式写入数据的前置步骤**——未配置下拉选项的单元格,`multipleValue` 写入会变成纯文本。 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。可以先用 `--dry-run` 预览。 + +## 命令 + +```bash +# 基础:设置单选下拉 +lark-cli sheets +set-dropdown --url "https://example.larksuite.com/sheets/shtxxxxxxxx" \ + --range "!A2:A100" --condition-values '["选项1", "选项2", "选项3"]' + +# 多选 + 颜色高亮 +lark-cli sheets +set-dropdown --spreadsheet-token "shtxxxxxxxx" \ + --range "!A2:A100" --condition-values '["选项1", "选项2", "选项3"]' \ + --multiple --highlight --colors '["#1FB6C1", "#F006C2", "#FB16C3"]' + +# 仅预览参数(不发请求) +lark-cli sheets +set-dropdown --url "https://..." --range "..." --condition-values '...' --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token` | 否 | 表格 token | +| `--range` | 是 | 范围(如 `!A2:A100`),单次最多 5000 行 x 100 列 | +| `--condition-values` | 是 | 下拉选项,JSON 数组(如 `'["选项1","选项2"]'`),最多 500 个,每个 ≤100 字符,不能包含逗号 | +| `--multiple` | 否 | 是否多选,默认 false | +| `--highlight` | 否 | 是否着色,默认 false | +| `--colors` | 否 | RGB 十六进制颜色 JSON 数组,需与 `--condition-values` 一一对应(`--highlight` 时必填) | +| `--dry-run` | 否 | 仅打印参数,不执行请求 | + +## 输出 + +JSON,包含 `code`(0=成功)和 `msg`。 + +## 典型流程 + +```bash +# 1. 先配置下拉选项 +lark-cli sheets +set-dropdown --url "" \ + --range "!J2:J100" --condition-values '["选项1","选项2"]' --multiple + +# 2. 再用 multipleValue 写入 +lark-cli sheets +write --url "" --sheet-id "" --range "J2" \ + --values '[[{"type":"multipleValue","values":["选项1","选项2"]}]]' +``` + +## 参考 + +- [lark-sheets-update-dropdown](lark-sheets-update-dropdown.md) — 更新下拉列表 +- [lark-sheets-get-dropdown](lark-sheets-get-dropdown.md) — 查询下拉列表 +- [lark-sheets-delete-dropdown](lark-sheets-delete-dropdown.md) — 删除下拉列表 diff --git a/skills/lark-sheets/references/lark-sheets-update-dropdown.md b/skills/lark-sheets/references/lark-sheets-update-dropdown.md new file mode 100644 index 00000000..f83b6091 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-update-dropdown.md @@ -0,0 +1,51 @@ + +# sheets +update-dropdown(更新下拉列表) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +update-dropdown`。 + +更新已有下拉列表的选项、颜色等配置。可同时更新多个范围。 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。可以先用 `--dry-run` 预览。 + +## 命令 + +```bash +lark-cli sheets +update-dropdown --url "https://example.larksuite.com/sheets/shtxxxxxxxx" \ + --sheet-id "" \ + --ranges '["!A1:A100", "!C1:C100"]' \ + --condition-values '["新选项1", "新选项2", "新选项3"]' + +# 更新为多选 + 着色 +lark-cli sheets +update-dropdown --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" \ + --ranges '["!A1:A100"]' \ + --condition-values '["选项A", "选项B"]' \ + --multiple --highlight --colors '["#1FB6C1", "#F006C2"]' +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | +| `--ranges` | 是 | 范围 JSON 数组(如 `'["sheetId!A1:A100"]'`) | +| `--condition-values` | 是 | 新的下拉选项,JSON 数组 | +| `--multiple` | 否 | 是否多选,默认 false | +| `--highlight` | 否 | 是否着色,默认 false | +| `--colors` | 否 | RGB 颜色 JSON 数组,需与 `--condition-values` 一一对应 | +| `--dry-run` | 否 | 仅打印参数,不执行请求 | + +## 输出 + +JSON,包含 `spreadsheetToken`、`sheetId`、`dataValidation`(选项值和颜色映射)。 + +## 参考 + +- [lark-sheets-set-dropdown](lark-sheets-set-dropdown.md) — 设置下拉列表 +- [lark-sheets-get-dropdown](lark-sheets-get-dropdown.md) — 查询下拉列表 +- [lark-sheets-delete-dropdown](lark-sheets-delete-dropdown.md) — 删除下拉列表