mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 17:45:15 +08:00
Merge pull request #1030 from zhengzhijiej-tech/fix/sheets-e2e-fixes-batch1
fix(sheets): align +workbook-create, +dropdown-*, +dim-move, +range-sort with server schema (restore --colors/--highlight)
This commit is contained in:
@@ -132,8 +132,8 @@ func TestBatchOp_BodyMatchesStandalone(t *testing.T) {
|
||||
{
|
||||
shortcut: "+range-sort",
|
||||
sc: RangeSort,
|
||||
args: []string{"--sheet-id", "sh1", "--range", "A1:D10", "--sort-keys", `[{"col":"B","order":"asc"}]`, "--has-header"},
|
||||
subInput: `{"sheet-id":"sh1","range":"A1:D10","sort-keys":[{"col":"B","order":"asc"}],"has-header":true}`,
|
||||
args: []string{"--sheet-id", "sh1", "--range", "A1:D10", "--sort-keys", `[{"column":"B","ascending":true}]`, "--has-header"},
|
||||
subInput: `{"sheet-id":"sh1","range":"A1:D10","sort-keys":[{"column":"B","ascending":true}],"has-header":true}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+sheet-create",
|
||||
|
||||
@@ -1867,7 +1867,7 @@
|
||||
"name": "options",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"required": "xor",
|
||||
"desc": "Options as a JSON array, e.g. `[\"opt1\",\"opt2\"]`; up to 500 items, each ≤100 chars, no commas",
|
||||
"input": [
|
||||
"file",
|
||||
@@ -1879,7 +1879,7 @@
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "RGB hex color array (e.g. `[\"#1FB6C1\",\"#F006C2\"]`); length must equal `--options`",
|
||||
"desc": "RGB hex pill colors for dropdown options (e.g. `[\"#1FB6C1\",\"#F006C2\"]`); maps to server `data_validation.highlight_colors`. Length may be **shorter than** `--options` (server cycles remaining slots through a built-in 10-color palette) but **must not exceed** it. Only takes effect when `--highlight` is also set.",
|
||||
"input": [
|
||||
"file",
|
||||
"stdin"
|
||||
@@ -1897,7 +1897,14 @@
|
||||
"kind": "own",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Color-highlight options; default `false`"
|
||||
"desc": "Enable pill-background color highlighting for dropdown options; default `false`. Maps to server `data_validation.enable_highlight`. When `true`, colors come from `--colors` in order; options beyond `--colors` length cycle through a built-in 10-color palette."
|
||||
},
|
||||
{
|
||||
"name": "source-range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Source range for listFromRange dropdown (A1 + sheet prefix, e.g. `Sheet1!T1:T3`); maps to server `data_validation.range` and auto-sets `data_validation.type='listFromRange'`. XOR with `--options`: pass `--options` for an inline list (type=list), pass this for a range reference (type=listFromRange). `--colors` length rule unchanged (≤ source range cell count); `--highlight` / `--multiple` behave the same."
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
@@ -2795,7 +2802,7 @@
|
||||
"name": "options",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"required": "xor",
|
||||
"desc": "Options as a JSON array (e.g. `[\"opt1\",\"opt2\"]`)",
|
||||
"input": [
|
||||
"file",
|
||||
@@ -2807,7 +2814,7 @@
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Color array (same length as `--options`)",
|
||||
"desc": "RGB hex pill colors for dropdown options (e.g. `[\"#1FB6C1\",\"#F006C2\"]`); maps to server `data_validation.highlight_colors`. Length may be **shorter than** `--options` (remaining slots cycle through a built-in palette) but **must not exceed** it. Only takes effect when `--highlight` is also set.",
|
||||
"input": [
|
||||
"file",
|
||||
"stdin"
|
||||
@@ -2825,7 +2832,14 @@
|
||||
"kind": "own",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Color-highlight options"
|
||||
"desc": "Enable pill-background color highlighting for dropdown options; default `false`. Maps to server `data_validation.enable_highlight`. Works with `--colors`; if `--colors` is omitted, all options cycle through a built-in 10-color palette."
|
||||
},
|
||||
{
|
||||
"name": "source-range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Source range for listFromRange dropdown (A1 + sheet prefix, e.g. `Sheet1!T1:T3`); maps to server `data_validation.range` and auto-sets `data_validation.type='listFromRange'`. XOR with `--options`: pass `--options` for an inline list (type=list), pass this for a range reference (type=listFromRange). `--colors` length rule unchanged (≤ source range cell count); `--highlight` / `--multiple` behave the same."
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
|
||||
@@ -558,7 +558,7 @@
|
||||
]
|
||||
},
|
||||
"items": {
|
||||
"description": "列表选项",
|
||||
"description": "列表选项(type='list' 时必填)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -603,6 +603,17 @@
|
||||
"help_text": {
|
||||
"description": "验证失败时显示的提示文本",
|
||||
"type": "string"
|
||||
},
|
||||
"enable_highlight": {
|
||||
"description": "是否开启下拉选项的胶囊背景色高亮,仅 type='list'/'listFromRange' 生效,默认 false。",
|
||||
"type": "boolean"
|
||||
},
|
||||
"highlight_colors": {
|
||||
"description": "下拉选项的胶囊背景色数组(十六进制,例如 [\"#FFE699\",\"#bff7d9\",\"#ffb3b3\"]),仅当 enable_highlight=true 时生效。按顺序对应(type='list' 对应 items;type='listFromRange' 按 range 内单元格行优先顺序,如 A1:A10 对应第 1-10 项;A1:B2 顺序为 A1,B1,A2,B2)。长度可以短于但不能长于;未指定项及不提供该字段时按内置 10 色色板循环补色。",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -1759,10 +1770,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"size"
|
||||
],
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
@@ -2789,10 +2796,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"size"
|
||||
],
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
@@ -3568,7 +3571,7 @@
|
||||
},
|
||||
"+dropdown-set": {
|
||||
"options": {
|
||||
"description": "列表选项",
|
||||
"description": "列表选项(type='list' 时必填)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -3577,7 +3580,7 @@
|
||||
},
|
||||
"+dropdown-update": {
|
||||
"options": {
|
||||
"description": "列表选项",
|
||||
"description": "列表选项(type='list' 时必填)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
||||
@@ -398,7 +398,7 @@ func TestExecute_RangeSort(t *testing.T) {
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--range", "A1:D50",
|
||||
"--has-header",
|
||||
"--sort-keys", `[{"col":"B","order":"asc"}]`,
|
||||
"--sort-keys", `[{"column":"B","ascending":true}]`,
|
||||
}, stub)
|
||||
if err != nil {
|
||||
t.Fatalf("execute failed: %v", err)
|
||||
|
||||
@@ -332,7 +332,7 @@ var DropdownUpdate = common.Shortcut{
|
||||
if _, err := validateDropdownRanges(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := validateDropdownOptionsColors(runtime); err != nil {
|
||||
if _, err := validateDropdownSourceOrOptions(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -188,14 +188,16 @@ func TestCellsBatchClear_Guards(t *testing.T) {
|
||||
|
||||
// TestDropdownUpdate_BatchPayload verifies the multi-range dropdown
|
||||
// update fans out into a single batch_update with one set_cell_range
|
||||
// op per range.
|
||||
// op per range. Also covers --colors / --highlight -> highlight_colors
|
||||
// / enable_highlight propagation through dropdownBatchInput.
|
||||
func TestDropdownUpdate_BatchPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, DropdownUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--ranges", `["sheet1!A2:A5","sheet1!C2:C5"]`,
|
||||
"--options", `["a","b","c"]`,
|
||||
"--multiple",
|
||||
"--colors", `["#FFE699","#bff7d9","#ffb3b3"]`,
|
||||
"--multiple", "--highlight",
|
||||
})
|
||||
input := decodeToolInput(t, body, "batch_update")
|
||||
ops, _ := input["operations"].([]interface{})
|
||||
@@ -215,8 +217,19 @@ func TestDropdownUpdate_BatchPayload(t *testing.T) {
|
||||
if dv == nil || dv["type"] != "list" {
|
||||
t.Errorf("op[%d] missing data_validation list: %#v", i, cell)
|
||||
}
|
||||
if dv["multiple_values"] != true {
|
||||
t.Errorf("op[%d] multiple_values = %v, want true", i, dv["multiple_values"])
|
||||
items, _ := dv["items"].([]interface{})
|
||||
if len(items) != 3 {
|
||||
t.Errorf("op[%d] data_validation.items length = %d, want 3", i, len(items))
|
||||
}
|
||||
if dv["support_multiple_values"] != true {
|
||||
t.Errorf("op[%d] support_multiple_values = %v, want true", i, dv["support_multiple_values"])
|
||||
}
|
||||
colors, _ := dv["highlight_colors"].([]interface{})
|
||||
if len(colors) != 3 {
|
||||
t.Errorf("op[%d] highlight_colors length = %d, want 3", i, len(colors))
|
||||
}
|
||||
if dv["enable_highlight"] != true {
|
||||
t.Errorf("op[%d] enable_highlight = %v, want true", i, dv["enable_highlight"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +598,23 @@ func rangeSortInput(runtime flagView, token, sheetID, sheetName string) (map[str
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// transform_range.sort_conditions[i] requires both `column` (string)
|
||||
// and `ascending` (bool); the server's own validation surfaces a
|
||||
// terse "required property X is missing" with no per-item context.
|
||||
// Pre-check here so the user sees which entry is malformed.
|
||||
for i, raw := range keys {
|
||||
item, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, common.FlagErrorf("--sort-keys[%d] must be an object {column, ascending}; got %T", i, raw)
|
||||
}
|
||||
col, _ := item["column"].(string)
|
||||
if strings.TrimSpace(col) == "" {
|
||||
return nil, common.FlagErrorf("--sort-keys[%d] missing required string field `column` (the column letter to sort by, e.g. \"C\")", i)
|
||||
}
|
||||
if _, ok := item["ascending"].(bool); !ok {
|
||||
return nil, common.FlagErrorf("--sort-keys[%d] missing required bool field `ascending`", i)
|
||||
}
|
||||
}
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "sort",
|
||||
|
||||
@@ -185,7 +185,7 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) {
|
||||
{
|
||||
name: "+range-sort multi-key with header",
|
||||
sc: RangeSort,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "A1:E100", "--has-header", "--sort-keys", `[{"col":"B","order":"asc"},{"col":"D","order":"desc"}]`},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "A1:E100", "--has-header", "--sort-keys", `[{"column":"B","ascending":true},{"column":"D","ascending":false}]`},
|
||||
toolName: "transform_range",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
@@ -194,8 +194,8 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) {
|
||||
"range": "A1:E100",
|
||||
"has_header": true,
|
||||
"sort_conditions": []interface{}{
|
||||
map[string]interface{}{"col": "B", "order": "asc"},
|
||||
map[string]interface{}{"col": "D", "order": "desc"},
|
||||
map[string]interface{}{"column": "B", "ascending": true},
|
||||
map[string]interface{}{"column": "D", "ascending": false},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -211,6 +211,41 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRangeSort_RejectsMalformedKeys verifies the pre-check that each
|
||||
// --sort-keys entry has both `column` (string) and `ascending` (bool);
|
||||
// previously the CLI passed any JSON through and the server bounced
|
||||
// with a terse "required property X missing" that didn't name the bad
|
||||
// entry.
|
||||
func TestRangeSort_RejectsMalformedKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
keys string
|
||||
want string
|
||||
}{
|
||||
{"missing column", `[{"ascending":true}]`, "missing required string field `column`"},
|
||||
{"missing ascending", `[{"column":"B"}]`, "missing required bool field `ascending`"},
|
||||
{"old vocab col/order", `[{"col":"B","order":"asc"}]`, "missing required string field `column`"},
|
||||
{"non-object item", `["B"]`, "must be an object"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
stdout, stderr, err := runShortcutCapturingErr(t, RangeSort, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--range", "A1:E10", "--sort-keys", c.keys, "--dry-run",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error; stdout=%s stderr=%s", stdout, stderr)
|
||||
}
|
||||
if !strings.Contains(stdout+stderr+err.Error(), c.want) {
|
||||
t.Errorf("want substring %q in error; got stdout=%s stderr=%s err=%v", c.want, stdout, stderr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResize_TypeAndSizeGuards(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
|
||||
@@ -215,8 +215,12 @@ func stripRowPrefixFromCsvOutput(out interface{}) interface{} {
|
||||
}
|
||||
|
||||
// DropdownGet wraps get_cell_ranges scoped to data_validation: read the
|
||||
// dropdown configuration on a range. The range carries its own sheet prefix
|
||||
// (e.g. "sheet1!A2:A100"), so no separate --sheet-id / --sheet-name is needed.
|
||||
// dropdown configuration on a range. The CLI accepts the range in the
|
||||
// sheet-prefixed form (e.g. "sheet1!A2:A100") for convenience; the
|
||||
// prefix is split client-side into sheet_name + bare A1 because the
|
||||
// get_cell_ranges tool wants sheet selector and ranges as separate
|
||||
// fields (ranges with the "sheet!" prefix gets the empty-sheet_id
|
||||
// rejection from the server).
|
||||
var DropdownGet = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+dropdown-get",
|
||||
@@ -257,10 +261,15 @@ var DropdownGet = common.Shortcut{
|
||||
}
|
||||
|
||||
func dropdownGetInput(runtime *common.RuntimeContext, token string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
// Validate already enforced the "Sheet!range" prefix, so the
|
||||
// split error path can't be reached here in practice.
|
||||
sheetName, bareRange, _ := splitSheetPrefixedRange(strings.TrimSpace(runtime.Str("range")))
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"ranges": []string{strings.TrimSpace(runtime.Str("range"))},
|
||||
"ranges": []string{bareRange},
|
||||
"include_styles": false,
|
||||
"value_render_option": "formatted_value",
|
||||
}
|
||||
sheetSelectorForToolInput(input, "", sheetName)
|
||||
return input
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ func TestReadDataShortcuts_DryRun(t *testing.T) {
|
||||
toolName: "get_cell_ranges",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
"ranges": []interface{}{"sheet1!A2:A100"},
|
||||
"sheet_name": "sheet1",
|
||||
"ranges": []interface{}{"A2:A100"},
|
||||
"include_styles": false,
|
||||
"value_render_option": "formatted_value",
|
||||
},
|
||||
|
||||
@@ -331,40 +331,85 @@ func dropdownSetInput(runtime flagView, token, sheetID, sheetName string) (map[s
|
||||
|
||||
// ─── shared dropdown helpers ──────────────────────────────────────────
|
||||
|
||||
// buildDropdownValidation packs --options / --colors / --multiple / --highlight
|
||||
// into the data_validation block expected by set_cell_range.
|
||||
// buildDropdownValidation packs --options or --source-range plus --colors /
|
||||
// --multiple / --highlight into the data_validation block expected by
|
||||
// set_cell_range. Field names follow the canonical
|
||||
// set_cell_range.data_validation schema:
|
||||
//
|
||||
// --options -> {type: "list", items: <strings>}
|
||||
// --source-range -> {type: "listFromRange", range: <A1+sheet prefix>}
|
||||
// --multiple -> support_multiple_values (bool)
|
||||
// --colors -> highlight_colors (string array, hex)
|
||||
// --highlight -> enable_highlight (bool)
|
||||
//
|
||||
// --options and --source-range are XOR (caller must pass exactly one).
|
||||
// --colors length may be shorter than the source size (options length or
|
||||
// source-range cell count) — server cycles remaining slots through a
|
||||
// built-in 10-color palette — but must not exceed it.
|
||||
func buildDropdownValidation(runtime flagView) (map[string]interface{}, error) {
|
||||
options, err := requireJSONArray(runtime, "options")
|
||||
sourceSize, dv, err := dropdownTypeAndItems(runtime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dv := map[string]interface{}{
|
||||
"type": "list",
|
||||
"values": options,
|
||||
}
|
||||
if runtime.Str("colors") != "" {
|
||||
colors, err := requireJSONArray(runtime, "colors")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(colors) != len(options) {
|
||||
return nil, common.FlagErrorf("--colors length (%d) must equal --options length (%d)", len(colors), len(options))
|
||||
if len(colors) > sourceSize {
|
||||
return nil, common.FlagErrorf("--colors length (%d) must not exceed dropdown source size (%d)", len(colors), sourceSize)
|
||||
}
|
||||
dv["colors"] = colors
|
||||
dv["highlight_colors"] = colors
|
||||
}
|
||||
if runtime.Bool("multiple") {
|
||||
dv["multiple_values"] = true
|
||||
dv["support_multiple_values"] = true
|
||||
}
|
||||
if runtime.Bool("highlight") {
|
||||
dv["highlight_options"] = true
|
||||
dv["enable_highlight"] = true
|
||||
}
|
||||
return dv, nil
|
||||
}
|
||||
|
||||
// validateDropdownOptionsColors validates --options is a JSON array and that
|
||||
// --colors (when set) has matching length. Used by +dropdown-update Validate.
|
||||
func validateDropdownOptionsColors(runtime flagView) (int, error) {
|
||||
options, err := requireJSONArray(runtime, "options")
|
||||
// dropdownTypeAndItems resolves the XOR between --options and --source-range
|
||||
// and returns (sourceSize, partial dv with type+items|range set). sourceSize
|
||||
// is the option count for `list` mode or the source-range cell count for
|
||||
// `listFromRange` mode — used to validate --colors length.
|
||||
func dropdownTypeAndItems(runtime flagView) (int, map[string]interface{}, error) {
|
||||
optsRaw := runtime.Str("options")
|
||||
sourceRange := strings.TrimSpace(runtime.Str("source-range"))
|
||||
switch {
|
||||
case optsRaw != "" && sourceRange != "":
|
||||
return 0, nil, common.FlagErrorf("--options and --source-range are mutually exclusive; pass exactly one")
|
||||
case optsRaw == "" && sourceRange == "":
|
||||
return 0, nil, common.FlagErrorf("one of --options (inline list) or --source-range (listFromRange) is required")
|
||||
case optsRaw != "":
|
||||
options, err := requireJSONArray(runtime, "options")
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return len(options), map[string]interface{}{
|
||||
"type": "list",
|
||||
"items": options,
|
||||
}, nil
|
||||
default: // sourceRange != ""
|
||||
rows, cols, err := rangeDimensions(sourceRange)
|
||||
if err != nil {
|
||||
return 0, nil, common.FlagErrorf("--source-range %q: %v", sourceRange, err)
|
||||
}
|
||||
return rows * cols, map[string]interface{}{
|
||||
"type": "listFromRange",
|
||||
"range": sourceRange,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// validateDropdownSourceOrOptions runs the XOR + --colors length check at
|
||||
// Validate time so +dropdown-update / +dropdown-delete can fail fast without
|
||||
// reaching the body-build step. Returns the dropdown source size (options
|
||||
// length for list mode, source-range cell count for listFromRange) so
|
||||
// callers can size their cells matrix.
|
||||
func validateDropdownSourceOrOptions(runtime flagView) (int, error) {
|
||||
sourceSize, _, err := dropdownTypeAndItems(runtime)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -373,11 +418,11 @@ func validateDropdownOptionsColors(runtime flagView) (int, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(colors) != len(options) {
|
||||
return 0, common.FlagErrorf("--colors length (%d) must equal --options length (%d)", len(colors), len(options))
|
||||
if len(colors) > sourceSize {
|
||||
return 0, common.FlagErrorf("--colors length (%d) must not exceed dropdown source size (%d)", len(colors), sourceSize)
|
||||
}
|
||||
}
|
||||
return len(options), nil
|
||||
return sourceSize, nil
|
||||
}
|
||||
|
||||
// ─── range parsing helpers ────────────────────────────────────────────
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestWriteCellsShortcuts_DryRun(t *testing.T) {
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--range", "A2:A4",
|
||||
"--options", `["a","b"]`,
|
||||
"--multiple", "--highlight",
|
||||
"--multiple",
|
||||
},
|
||||
toolName: "set_cell_range",
|
||||
wantInput: map[string]interface{}{
|
||||
@@ -118,11 +118,15 @@ func TestWriteCellsShortcuts_DryRun(t *testing.T) {
|
||||
|
||||
// TestDropdownSet_CellsShape inspects the 3×1 matrix produced from
|
||||
// --range A2:A4 to confirm the data_validation prototype is replicated.
|
||||
// Also covers --colors / --highlight emitting the canonical
|
||||
// `highlight_colors` / `enable_highlight` field names (not the legacy
|
||||
// `colors` / `highlight_options`).
|
||||
func TestDropdownSet_CellsShape(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, DropdownSet, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--range", "A2:A4", "--options", `["a","b"]`, "--multiple",
|
||||
"--colors", `["#FFE699","#bff7d9"]`, "--highlight",
|
||||
})
|
||||
input := decodeToolInput(t, body, "set_cell_range")
|
||||
cells, _ := input["cells"].([]interface{})
|
||||
@@ -143,9 +147,164 @@ func TestDropdownSet_CellsShape(t *testing.T) {
|
||||
if dv["type"] != "list" {
|
||||
t.Errorf("row %d data_validation.type = %v, want list", i, dv["type"])
|
||||
}
|
||||
if dv["multiple_values"] != true {
|
||||
t.Errorf("row %d data_validation.multiple_values = %v, want true", i, dv["multiple_values"])
|
||||
items, _ := dv["items"].([]interface{})
|
||||
if len(items) != 2 || items[0] != "a" || items[1] != "b" {
|
||||
t.Errorf("row %d data_validation.items = %#v, want [\"a\",\"b\"]", i, dv["items"])
|
||||
}
|
||||
if dv["support_multiple_values"] != true {
|
||||
t.Errorf("row %d data_validation.support_multiple_values = %v, want true", i, dv["support_multiple_values"])
|
||||
}
|
||||
if _, hasLegacy := dv["multiple_values"]; hasLegacy {
|
||||
t.Errorf("row %d data_validation should not emit legacy `multiple_values`", i)
|
||||
}
|
||||
colors, _ := dv["highlight_colors"].([]interface{})
|
||||
if len(colors) != 2 || colors[0] != "#FFE699" || colors[1] != "#bff7d9" {
|
||||
t.Errorf("row %d data_validation.highlight_colors = %#v, want [\"#FFE699\",\"#bff7d9\"]", i, dv["highlight_colors"])
|
||||
}
|
||||
if dv["enable_highlight"] != true {
|
||||
t.Errorf("row %d data_validation.enable_highlight = %v, want true", i, dv["enable_highlight"])
|
||||
}
|
||||
if _, hasLegacy := dv["colors"]; hasLegacy {
|
||||
t.Errorf("row %d data_validation should not emit legacy `colors`", i)
|
||||
}
|
||||
if _, hasLegacy := dv["highlight_options"]; hasLegacy {
|
||||
t.Errorf("row %d data_validation should not emit legacy `highlight_options`", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropdownSet_ColorsLongerThanOptions checks the early Validate-time
|
||||
// error when --colors length exceeds the dropdown source size (options
|
||||
// length in list mode). Equal-or-shorter lengths are accepted (server
|
||||
// cycles the rest through a built-in palette).
|
||||
func TestDropdownSet_ColorsLongerThanOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, stderr, err := runShortcutCapturingErr(t, DropdownSet, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--range", "A2:A4",
|
||||
"--options", `["a","b"]`,
|
||||
"--colors", `["#FFE699","#bff7d9","#ffb3b3"]`,
|
||||
"--dry-run",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected --colors length error, got nil")
|
||||
}
|
||||
if !strings.Contains(stderr, "must not exceed dropdown source size") && !strings.Contains(err.Error(), "must not exceed dropdown source size") {
|
||||
t.Errorf("error message missing length-overflow hint:\nerr=%v\nstderr=%s", err, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropdownSet_ColorsShorterAccepted verifies the partial-colors case:
|
||||
// fewer colors than options is legal — array is forwarded as-is and the
|
||||
// server fills remaining slots from its default palette.
|
||||
func TestDropdownSet_ColorsShorterAccepted(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, DropdownSet, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--range", "A2:A4",
|
||||
"--options", `["a","b","c","d"]`,
|
||||
"--colors", `["#FFE699","#bff7d9"]`,
|
||||
})
|
||||
input := decodeToolInput(t, body, "set_cell_range")
|
||||
cells, _ := input["cells"].([]interface{})
|
||||
row0, _ := cells[0].([]interface{})
|
||||
cell, _ := row0[0].(map[string]interface{})
|
||||
dv, _ := cell["data_validation"].(map[string]interface{})
|
||||
colors, _ := dv["highlight_colors"].([]interface{})
|
||||
if len(colors) != 2 {
|
||||
t.Errorf("highlight_colors length = %d, want 2 (forwarded as-is)", len(colors))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropdownSet_ListFromRange verifies --source-range emits
|
||||
// data_validation.type=listFromRange + data_validation.range, paired with
|
||||
// --colors / --highlight propagating to highlight_colors / enable_highlight.
|
||||
func TestDropdownSet_ListFromRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, DropdownSet, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--range", "B2:B21",
|
||||
"--source-range", "Sheet1!T1:T3",
|
||||
"--colors", `["#cce8ff","#ffd6e7","#e6e6e6"]`,
|
||||
"--highlight",
|
||||
})
|
||||
input := decodeToolInput(t, body, "set_cell_range")
|
||||
cells, _ := input["cells"].([]interface{})
|
||||
row0, _ := cells[0].([]interface{})
|
||||
cell, _ := row0[0].(map[string]interface{})
|
||||
dv, _ := cell["data_validation"].(map[string]interface{})
|
||||
if dv["type"] != "listFromRange" {
|
||||
t.Errorf("data_validation.type = %v, want listFromRange", dv["type"])
|
||||
}
|
||||
if dv["range"] != "Sheet1!T1:T3" {
|
||||
t.Errorf("data_validation.range = %v, want Sheet1!T1:T3 (verbatim, server normalizes)", dv["range"])
|
||||
}
|
||||
if _, hasItems := dv["items"]; hasItems {
|
||||
t.Errorf("listFromRange mode should not emit `items`: %#v", dv)
|
||||
}
|
||||
if dv["enable_highlight"] != true {
|
||||
t.Errorf("data_validation.enable_highlight = %v, want true", dv["enable_highlight"])
|
||||
}
|
||||
colors, _ := dv["highlight_colors"].([]interface{})
|
||||
if len(colors) != 3 {
|
||||
t.Errorf("highlight_colors length = %d, want 3", len(colors))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropdownSet_ListFromRange_ColorsLongerThanCells rejects --colors
|
||||
// longer than the source range cell count (T1:T3 has 3 cells, 4 colors
|
||||
// must be refused).
|
||||
func TestDropdownSet_ListFromRange_ColorsLongerThanCells(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, stderr, err := runShortcutCapturingErr(t, DropdownSet, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--range", "B2:B21",
|
||||
"--source-range", "Sheet1!T1:T3",
|
||||
"--colors", `["#a","#b","#c","#d"]`,
|
||||
"--highlight",
|
||||
"--dry-run",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected --colors length error, got nil")
|
||||
}
|
||||
if !strings.Contains(stderr, "must not exceed dropdown source size") && !strings.Contains(err.Error(), "must not exceed dropdown source size") {
|
||||
t.Errorf("error message missing source-size hint:\nerr=%v\nstderr=%s", err, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropdownSet_XorBothSet rejects passing both --options and
|
||||
// --source-range.
|
||||
func TestDropdownSet_XorBothSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, stderr, err := runShortcutCapturingErr(t, DropdownSet, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--range", "B2:B21",
|
||||
"--options", `["a","b"]`,
|
||||
"--source-range", "Sheet1!T1:T3",
|
||||
"--dry-run",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected XOR error, got nil")
|
||||
}
|
||||
if !strings.Contains(stderr, "mutually exclusive") && !strings.Contains(err.Error(), "mutually exclusive") {
|
||||
t.Errorf("error message missing XOR hint:\nerr=%v\nstderr=%s", err, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropdownSet_XorNeitherSet rejects passing neither --options nor
|
||||
// --source-range.
|
||||
func TestDropdownSet_XorNeitherSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, stderr, err := runShortcutCapturingErr(t, DropdownSet, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--range", "B2:B21",
|
||||
"--dry-run",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected required-one error, got nil")
|
||||
}
|
||||
if !strings.Contains(stderr, "one of --options") && !strings.Contains(err.Error(), "one of --options") {
|
||||
t.Errorf("error message missing required-one hint:\nerr=%v\nstderr=%s", err, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
当同一工具需要对多个区域重复调用时,**必须**改用 `+batch-update` 合并为单次请求——`+batch-update` 是原子提交(要么全成功要么整批回滚);逐个调用非原子,中途失败会留下半成品。
|
||||
|
||||
**`+dropdown-update` 的选项模式(`--options` / `--source-range` 二选一)+ 配色规则**(`--colors` 长度可短不能长、必须配 `--highlight=true` 才生效、不传按内置 10 色色板循环补色)见 [`lark-sheets-write-cells`](./lark-sheets-write-cells.md) 的「Dropdown 选项 + 配色」节,本 skill 不重复。`+dropdown-delete` 不涉及这些 flag。
|
||||
|
||||
## Shortcuts
|
||||
|
||||
| Shortcut | Risk | 分组 |
|
||||
@@ -69,10 +71,11 @@ _公共:URL/token(无 sheet 定位) · 系统:`--dry-run`_
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--ranges` | string + File + Stdin(简单 JSON) | required | 目标范围 JSON 数组(如 `["sheet1!A2:A100"]`),每项必须带 sheet 前缀 |
|
||||
| `--options` | string + File + Stdin(复合 JSON) | required | 选项 JSON 数组(如 `["opt1","opt2"]`) |
|
||||
| `--colors` | string + File + Stdin(简单 JSON) | optional | 颜色数组(与 `--options` 等长) |
|
||||
| `--options` | string + File + Stdin(复合 JSON) | xor | 选项 JSON 数组(如 `["opt1","opt2"]`) |
|
||||
| `--colors` | string + File + Stdin(简单 JSON) | optional | 下拉选项的胶囊背景色,RGB hex 数组(如 `["#1FB6C1","#F006C2"]`)。映射到 server `data_validation.highlight_colors`。长度可以**短于** `--options`(剩余项按内置色板补色)但**不能长于**。仅当 `--highlight` 也传时才生效。 |
|
||||
| `--multiple` | bool | optional | 启用多选 |
|
||||
| `--highlight` | bool | optional | 选项配色 |
|
||||
| `--highlight` | bool | optional | 开启下拉选项的胶囊背景色高亮;默认 `false`。映射到 server `data_validation.enable_highlight`。需配合 `--colors` 使用——不传 `--colors` 时全部选项按内置 10 色色板循环。 |
|
||||
| `--source-range` | string | xor | listFromRange 模式的下拉源 range,A1 表示法 + sheet 前缀(如 `Sheet1!T1:T3`)。映射到 server `data_validation.range`,搭配 server `data_validation.type='listFromRange'` 自动生效。跟 `--options` 二选一:传 `--options` 走 inline 列表(type=list),传本 flag 走 range 引用(type=listFromRange)。`--colors` 长度规则不变(≤ 源 range 单元格数),`--highlight` / `--multiple` 行为相同。 |
|
||||
|
||||
### `+dropdown-delete`
|
||||
|
||||
@@ -115,7 +118,7 @@ _单元格边框配置,含 top/bottom/left/right 四个方向,每个方向
|
||||
|
||||
### `+dropdown-update` `--options`
|
||||
|
||||
_列表选项_
|
||||
_列表选项(type='list' 时必填)_
|
||||
|
||||
**数组项**(类型 string):
|
||||
- 标量:string
|
||||
|
||||
@@ -147,9 +147,9 @@ _公共四件套 · 系统:`--yes`、`--dry-run`_
|
||||
_创建/更新的图表属性_
|
||||
|
||||
**顶层字段**:
|
||||
- `position` (object) — 必填 { row: number, col: string }
|
||||
- `position` (object?) — 必填 { row: number, col: string }
|
||||
- `offset` (object?) — 可选 { row_offset?: number, col_offset?: number }
|
||||
- `size` (object) — 必填 { width: number, height: number }
|
||||
- `size` (object?) — 必填 { width: number, height: number }
|
||||
- `snapshot` (object?) — 图表快照配置 { title?: object, subTitle?: object, style?: object, legend?: oneOf, plotArea?: object, …共 6 项 }
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -120,6 +120,59 @@ Step 2: `+cells-set` — range="A2", cells 含 value + cell_styles + border_styl
|
||||
|
||||
**判断是不是"新行"**:写入 range 超出 `+csv-get` 返回的 `current_region` 右 / 下边界(如 `current_region=A1:H10`、写 `A11:H11`)即新行,必须按上述做法补边框。
|
||||
|
||||
## Dropdown 选项 + 配色(`+dropdown-set` / `+dropdown-update`)
|
||||
|
||||
### 选项怎么来:`--options` 与 `--source-range` 二选一
|
||||
|
||||
| flag | 选项来源 | 适用场景 |
|
||||
|---|---|---|
|
||||
| `--options '["a","b","c"]'` | 写在命令里的固定列表 | 选项集是常量、不需要事后维护 |
|
||||
| `--source-range 'Sheet1!T1:T3'` | 已有单元格里的值 | 选项要跟数据动态同步;想维护一张「枚举值」列后多处引用 |
|
||||
|
||||
两个 flag **必须传一个、且只能传一个**——同时传或都不传,CLI 会立刻报错。`--source-range` 用 A1 + sheet 前缀写法(如 `Sheet1!T1:T3`),可以指同 sheet 也可以指其它 sheet(如 `Refs!A1:A10`)。
|
||||
|
||||
### 配色:`--colors` 与 `--highlight`(与选项模式无关)
|
||||
|
||||
- `--highlight`:开下拉选项的胶囊背景色总开关;不传默认 `false`、无配色。
|
||||
- `--colors '["#hex","#hex",...]'`:每个选项对应一种胶囊背景色;只在 `--highlight` 也传时生效。
|
||||
|
||||
长度规则:
|
||||
- `--colors` 长度**可以短于**选项数(list 模式短于 `--options` 长度,listFromRange 模式短于 `--source-range` 的单元格数),未指定的选项按内置 10 色色板循环补色;
|
||||
- `--colors` 长度**不能长于**选项数——CLI 端 Validate 阶段就会拦截,错误信息形如 `--colors length (4) must not exceed dropdown source size (3)`。
|
||||
|
||||
排列组合:
|
||||
- 想要纯文本下拉、无配色 → 都不传 `--highlight` / `--colors`;
|
||||
- 想要每个选项指定确切颜色 → 同时传 `--highlight` 和 `--colors`;
|
||||
- 想要所有选项都有颜色但不指定具体色 → 只传 `--highlight`,颜色按内置色板循环。
|
||||
|
||||
### 最小用例
|
||||
|
||||
**`--options` 模式**(4 个选项配 3 个颜色——前 3 个指定色,第 4 个按内置色板补):
|
||||
|
||||
```
|
||||
lark-cli sheets +dropdown-set \
|
||||
--url https://... --sheet-id <id> \
|
||||
--range A2:A100 \
|
||||
--options '["待开始","进行中","已完成","已取消"]' \
|
||||
--colors '["#bff7d9","#FFE699","#bacefd"]' \
|
||||
--highlight
|
||||
```
|
||||
|
||||
**`--source-range` 模式**(先在 Sheet1!T1:T3 维护「男/女/保密」三行,再让 B2:B21 引用它):
|
||||
|
||||
```
|
||||
lark-cli sheets +dropdown-set \
|
||||
--url https://... --sheet-id <id> \
|
||||
--range B2:B21 \
|
||||
--source-range 'Sheet1!T1:T3' \
|
||||
--colors '["#cce8ff","#ffd6e7","#e6e6e6"]' \
|
||||
--highlight
|
||||
```
|
||||
|
||||
> ⚠️ **`--source-range` 必须带 sheet 前缀**(即使跟 `--range` 同 sheet)。注意一个坑:回读这种 listFromRange 下拉单元格时,`data_validation.range` 看起来不带 sheet 前缀(形如 `$T$1:$T$3`),如果要把读出来的 range 反过来写回 `--source-range`,**必须自己重新补上 sheet 前缀**,否则会被拒。
|
||||
|
||||
`+dropdown-update`(多 range 批量更新)的所有 flag 语义与 `+dropdown-set` 完全一致;只是目标 `--ranges` 由单值变成 JSON 数组(每项带 sheet 前缀),同一份选项 + 配色应用到所有 range。
|
||||
|
||||
## 工具选择
|
||||
|
||||
本 skill 提供以下 CLI shortcut,按数据来源 + 内容形态选:
|
||||
@@ -198,10 +251,11 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--range` | string | required | 目标范围(A1 格式,如 `A2:A100`) |
|
||||
| `--options` | string + File + Stdin(复合 JSON) | required | 选项 JSON 数组 `["opt1","opt2"]`;最多 500 项,每项 ≤100 字符,不含逗号 |
|
||||
| `--colors` | string + File + Stdin(简单 JSON) | optional | RGB hex 颜色数组(如 `["#1FB6C1","#F006C2"]`),长度必须与 `--options` 一致 |
|
||||
| `--options` | string + File + Stdin(复合 JSON) | xor | 选项 JSON 数组 `["opt1","opt2"]`;最多 500 项,每项 ≤100 字符,不含逗号 |
|
||||
| `--colors` | string + File + Stdin(简单 JSON) | optional | 下拉选项的胶囊背景色,RGB hex 数组,如 `["#1FB6C1","#F006C2"]`。映射到 server `data_validation.highlight_colors`。长度可以**短于** `--options`(剩余项 server 按内置 10 色色板循环补色),但**不能长于**。仅当 `--highlight` 也传时才生效;单独传本 flag 不显示高亮色。 |
|
||||
| `--multiple` | bool | optional | 启用多选;默认 `false` |
|
||||
| `--highlight` | bool | optional | 选项配色显示;默认 `false` |
|
||||
| `--highlight` | bool | optional | 开启下拉选项的胶囊背景色高亮;默认 `false`。映射到 server `data_validation.enable_highlight`。不传或为 `false` 时所有选项无背景色;为 `true` 时按 `--colors` 顺序上色,未在 `--colors` 中提供的选项使用内置 10 色色板循环补色。 |
|
||||
| `--source-range` | string | xor | listFromRange 模式的下拉源 range,A1 表示法 + sheet 前缀(如 `Sheet1!T1:T3`)。映射到 server `data_validation.range`,搭配 server `data_validation.type='listFromRange'` 自动生效。跟 `--options` 二选一:传 `--options` 走 inline 列表(type=list),传本 flag 走 range 引用(type=listFromRange)。`--colors` 长度规则不变(≤ 源 range 单元格数),`--highlight` / `--multiple` 行为相同。 |
|
||||
|
||||
### `+csv-put`
|
||||
|
||||
@@ -228,7 +282,7 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
- `border_styles` (object?) — 单元格边框配置,含 top/bottom/left/right 四个方向,每个方向的结构相同(见 top) { top?: object, bottom?: object, left?: object, right?: object }
|
||||
- `rich_text` (array<object>?) — 富文本内容 each: { type: enum, text: string, style?: object, link?: string, mention_token?: string, …共 17 项 }
|
||||
- `multiple_values` (array<object>?) — 多值内容,用于支持多选的列表验证单元格 each: { value: oneOf, format?: string }
|
||||
- `data_validation` (object?) — 数据验证配置 { type: enum, items?: array<string>, range?: string, operator?: enum, values?: array<oneOf>, …共 7 项 }
|
||||
- `data_validation` (object?) — 数据验证配置 { type: enum, items?: array<string>, range?: string, operator?: enum, values?: array<oneOf>, …共 9 项 }
|
||||
|
||||
### `+cells-set-style` `--border-styles`
|
||||
|
||||
@@ -242,7 +296,7 @@ _单元格边框配置,含 top/bottom/left/right 四个方向,每个方向
|
||||
|
||||
### `+dropdown-set` `--options`
|
||||
|
||||
_列表选项_
|
||||
_列表选项(type='list' 时必填)_
|
||||
|
||||
**数组项**(类型 string):
|
||||
- 标量:string
|
||||
|
||||
Reference in New Issue
Block a user