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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions shortcuts/sheets/batch_op_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 20 additions & 6 deletions shortcuts/sheets/data/flag-defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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",
Expand Down
25 changes: 14 additions & 11 deletions shortcuts/sheets/data/flag-schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@
]
},
"items": {
"description": "列表选项",
"description": "列表选项(type='list' 时必填)",
"type": "array",
"items": {
"type": "string"
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -1759,10 +1770,6 @@
}
}
},
"required": [
"position",
"size"
],
"additionalProperties": {}
}
},
Expand Down Expand Up @@ -2789,10 +2796,6 @@
}
}
},
"required": [
"position",
"size"
],
"additionalProperties": {}
}
},
Expand Down Expand Up @@ -3568,7 +3571,7 @@
},
"+dropdown-set": {
"options": {
"description": "列表选项",
"description": "列表选项(type='list' 时必填)",
"type": "array",
"items": {
"type": "string"
Expand All @@ -3577,7 +3580,7 @@
},
"+dropdown-update": {
"options": {
"description": "列表选项",
"description": "列表选项(type='list' 时必填)",
"type": "array",
"items": {
"type": "string"
Expand Down
2 changes: 1 addition & 1 deletion shortcuts/sheets/execute_paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion shortcuts/sheets/lark_sheet_batch_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 17 additions & 4 deletions shortcuts/sheets/lark_sheet_batch_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand All @@ -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"])
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions shortcuts/sheets/lark_sheet_range_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 38 additions & 3 deletions shortcuts/sheets/lark_sheet_range_operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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},
},
},
},
Expand All @@ -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 {
Expand Down
17 changes: 13 additions & 4 deletions shortcuts/sheets/lark_sheet_read_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
3 changes: 2 additions & 1 deletion shortcuts/sheets/lark_sheet_read_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
Loading
Loading