From 32f08695331c9ca943d9eb0c02838e2badb71323 Mon Sep 17 00:00:00 2001 From: / Date: Sat, 11 Apr 2026 19:17:27 +0800 Subject: [PATCH] feat(sheets): add filter view and condition shortcuts Add 10 new sheet shortcuts for filter view management: Filter views: - +create-filter-view, +update-filter-view, +list-filter-views - +get-filter-view, +delete-filter-view Filter view conditions: - +create-filter-view-condition, +update-filter-view-condition - +list-filter-view-conditions, +get-filter-view-condition - +delete-filter-view-condition Includes unit tests (39 cases, 88-93% coverage) and skill reference docs. --- shortcuts/sheets/sheet_filter_view.go | 239 +++++++ .../sheets/sheet_filter_view_condition.go | 261 ++++++++ shortcuts/sheets/sheet_filter_view_test.go | 628 ++++++++++++++++++ shortcuts/sheets/shortcuts.go | 10 + skills/lark-sheets/SKILL.md | 10 + ...ark-sheets-create-filter-view-condition.md | 42 ++ .../lark-sheets-create-filter-view.md | 42 ++ ...ark-sheets-delete-filter-view-condition.md | 26 + .../lark-sheets-delete-filter-view.md | 25 + .../lark-sheets-get-filter-view-condition.md | 27 + .../references/lark-sheets-get-filter-view.md | 26 + ...lark-sheets-list-filter-view-conditions.md | 28 + .../lark-sheets-list-filter-views.md | 26 + ...ark-sheets-update-filter-view-condition.md | 27 + .../lark-sheets-update-filter-view.md | 24 + 15 files changed, 1441 insertions(+) create mode 100644 shortcuts/sheets/sheet_filter_view.go create mode 100644 shortcuts/sheets/sheet_filter_view_condition.go create mode 100644 shortcuts/sheets/sheet_filter_view_test.go create mode 100644 skills/lark-sheets/references/lark-sheets-create-filter-view-condition.md create mode 100644 skills/lark-sheets/references/lark-sheets-create-filter-view.md create mode 100644 skills/lark-sheets/references/lark-sheets-delete-filter-view-condition.md create mode 100644 skills/lark-sheets/references/lark-sheets-delete-filter-view.md create mode 100644 skills/lark-sheets/references/lark-sheets-get-filter-view-condition.md create mode 100644 skills/lark-sheets/references/lark-sheets-get-filter-view.md create mode 100644 skills/lark-sheets/references/lark-sheets-list-filter-view-conditions.md create mode 100644 skills/lark-sheets/references/lark-sheets-list-filter-views.md create mode 100644 skills/lark-sheets/references/lark-sheets-update-filter-view-condition.md create mode 100644 skills/lark-sheets/references/lark-sheets-update-filter-view.md diff --git a/shortcuts/sheets/sheet_filter_view.go b/shortcuts/sheets/sheet_filter_view.go new file mode 100644 index 00000000..3794d166 --- /dev/null +++ b/shortcuts/sheets/sheet_filter_view.go @@ -0,0 +1,239 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "fmt" + + "github.com/larksuite/cli/internal/validate" + "github.com/larksuite/cli/shortcuts/common" +) + +func filterViewBasePath(token, sheetID string) string { + return fmt.Sprintf("/open-apis/sheets/v3/spreadsheets/%s/sheets/%s/filter_views", + validate.EncodePathSegment(token), validate.EncodePathSegment(sheetID)) +} + +func filterViewItemPath(token, sheetID, filterViewID string) string { + return fmt.Sprintf("%s/%s", filterViewBasePath(token, sheetID), validate.EncodePathSegment(filterViewID)) +} + +func validateFilterViewToken(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 +} + +var SheetCreateFilterView = common.Shortcut{ + Service: "sheets", + Command: "+create-filter-view", + Description: "Create a filter view", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL (required if --spreadsheet-token is not set)"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token (required if --url is not set)"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "range", Desc: "filter range (e.g. sheetId!A1:H14)", Required: true}, + {Name: "filter-view-name", Desc: "display name (max 100 chars)"}, + {Name: "filter-view-id", Desc: "custom 10-char alphanumeric ID (auto-generated if omitted)"}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + _, err := validateFilterViewToken(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateFilterViewToken(runtime) + body := map[string]interface{}{"range": runtime.Str("range")} + if s := runtime.Str("filter-view-name"); s != "" { + body["filter_view_name"] = s + } + if s := runtime.Str("filter-view-id"); s != "" { + body["filter_view_id"] = s + } + return common.NewDryRunAPI(). + POST("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/filter_views"). + Body(body).Set("token", token).Set("sheet_id", runtime.Str("sheet-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateFilterViewToken(runtime) + body := map[string]interface{}{"range": runtime.Str("range")} + if s := runtime.Str("filter-view-name"); s != "" { + body["filter_view_name"] = s + } + if s := runtime.Str("filter-view-id"); s != "" { + body["filter_view_id"] = s + } + data, err := runtime.CallAPI("POST", filterViewBasePath(token, runtime.Str("sheet-id")), nil, body) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +var SheetUpdateFilterView = common.Shortcut{ + Service: "sheets", + Command: "+update-filter-view", + Description: "Update a filter view", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL (required if --spreadsheet-token is not set)"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token (required if --url is not set)"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "filter-view-id", Desc: "filter view ID", Required: true}, + {Name: "range", Desc: "new filter range"}, + {Name: "filter-view-name", Desc: "new display name (max 100 chars)"}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := validateFilterViewToken(runtime); err != nil { + return err + } + if !runtime.Cmd.Flags().Changed("range") && + !runtime.Cmd.Flags().Changed("filter-view-name") { + return common.FlagErrorf("specify at least one of --range or --filter-view-name") + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateFilterViewToken(runtime) + body := map[string]interface{}{} + if s := runtime.Str("range"); s != "" { + body["range"] = s + } + if s := runtime.Str("filter-view-name"); s != "" { + body["filter_view_name"] = s + } + return common.NewDryRunAPI(). + PATCH("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/filter_views/:filter_view_id"). + Body(body).Set("token", token).Set("sheet_id", runtime.Str("sheet-id")).Set("filter_view_id", runtime.Str("filter-view-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateFilterViewToken(runtime) + body := map[string]interface{}{} + if s := runtime.Str("range"); s != "" { + body["range"] = s + } + if s := runtime.Str("filter-view-name"); s != "" { + body["filter_view_name"] = s + } + data, err := runtime.CallAPI("PATCH", filterViewItemPath(token, runtime.Str("sheet-id"), runtime.Str("filter-view-id")), nil, body) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +var SheetListFilterViews = common.Shortcut{ + Service: "sheets", + Command: "+list-filter-views", + Description: "List all filter views in a sheet", + Risk: "read", + Scopes: []string{"sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL (required if --spreadsheet-token is not set)"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token (required if --url is not set)"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + _, err := validateFilterViewToken(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateFilterViewToken(runtime) + return common.NewDryRunAPI(). + GET("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/filter_views/query"). + Set("token", token).Set("sheet_id", runtime.Str("sheet-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateFilterViewToken(runtime) + data, err := runtime.CallAPI("GET", filterViewBasePath(token, runtime.Str("sheet-id"))+"/query", nil, nil) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +var SheetGetFilterView = common.Shortcut{ + Service: "sheets", + Command: "+get-filter-view", + Description: "Get a filter view by ID", + Risk: "read", + Scopes: []string{"sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL (required if --spreadsheet-token is not set)"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token (required if --url is not set)"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "filter-view-id", Desc: "filter view ID", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + _, err := validateFilterViewToken(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateFilterViewToken(runtime) + return common.NewDryRunAPI(). + GET("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/filter_views/:filter_view_id"). + Set("token", token).Set("sheet_id", runtime.Str("sheet-id")).Set("filter_view_id", runtime.Str("filter-view-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateFilterViewToken(runtime) + data, err := runtime.CallAPI("GET", filterViewItemPath(token, runtime.Str("sheet-id"), runtime.Str("filter-view-id")), nil, nil) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +var SheetDeleteFilterView = common.Shortcut{ + Service: "sheets", + Command: "+delete-filter-view", + Description: "Delete a filter view", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL (required if --spreadsheet-token is not set)"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token (required if --url is not set)"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "filter-view-id", Desc: "filter view ID", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + _, err := validateFilterViewToken(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateFilterViewToken(runtime) + return common.NewDryRunAPI(). + DELETE("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/filter_views/:filter_view_id"). + Set("token", token).Set("sheet_id", runtime.Str("sheet-id")).Set("filter_view_id", runtime.Str("filter-view-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateFilterViewToken(runtime) + data, err := runtime.CallAPI("DELETE", filterViewItemPath(token, runtime.Str("sheet-id"), runtime.Str("filter-view-id")), nil, nil) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} diff --git a/shortcuts/sheets/sheet_filter_view_condition.go b/shortcuts/sheets/sheet_filter_view_condition.go new file mode 100644 index 00000000..043809ee --- /dev/null +++ b/shortcuts/sheets/sheet_filter_view_condition.go @@ -0,0 +1,261 @@ +// 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 filterViewConditionBasePath(token, sheetID, filterViewID string) string { + return fmt.Sprintf("%s/conditions", filterViewItemPath(token, sheetID, filterViewID)) +} + +func filterViewConditionItemPath(token, sheetID, filterViewID, conditionID string) string { + return fmt.Sprintf("%s/%s", filterViewConditionBasePath(token, sheetID, filterViewID), validate.EncodePathSegment(conditionID)) +} + +var SheetCreateFilterViewCondition = common.Shortcut{ + Service: "sheets", + Command: "+create-filter-view-condition", + Description: "Create a filter condition on a filter view", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL (required if --spreadsheet-token is not set)"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token (required if --url is not set)"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "filter-view-id", Desc: "filter view ID", Required: true}, + {Name: "condition-id", Desc: "column letter (e.g. E)", Required: true}, + {Name: "filter-type", Desc: "filter type: hiddenValue, number, text, color", Required: true}, + {Name: "compare-type", Desc: "comparison operator (e.g. less, beginsWith, between)"}, + {Name: "expected", Desc: "filter values JSON array (e.g. [\"6\"])", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := validateFilterViewToken(runtime); err != nil { + return err + } + return validateExpectedFlag(runtime.Str("expected")) + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateFilterViewToken(runtime) + body := buildConditionBody(runtime, true) + return common.NewDryRunAPI(). + POST("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/filter_views/:filter_view_id/conditions"). + Body(body).Set("token", token).Set("sheet_id", runtime.Str("sheet-id")).Set("filter_view_id", runtime.Str("filter-view-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateFilterViewToken(runtime) + body := buildConditionBody(runtime, true) + data, err := runtime.CallAPI("POST", filterViewConditionBasePath(token, runtime.Str("sheet-id"), runtime.Str("filter-view-id")), nil, body) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +var SheetUpdateFilterViewCondition = common.Shortcut{ + Service: "sheets", + Command: "+update-filter-view-condition", + Description: "Update a filter condition on a filter view", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL (required if --spreadsheet-token is not set)"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token (required if --url is not set)"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "filter-view-id", Desc: "filter view ID", Required: true}, + {Name: "condition-id", Desc: "column letter (e.g. E)", Required: true}, + {Name: "filter-type", Desc: "filter type: hiddenValue, number, text, color"}, + {Name: "compare-type", Desc: "comparison operator"}, + {Name: "expected", Desc: "filter values JSON array"}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := validateFilterViewToken(runtime); err != nil { + return err + } + if !runtime.Cmd.Flags().Changed("filter-type") && + !runtime.Cmd.Flags().Changed("compare-type") && + !runtime.Cmd.Flags().Changed("expected") { + return common.FlagErrorf("specify at least one of --filter-type, --compare-type, or --expected") + } + if s := runtime.Str("expected"); s != "" { + return validateExpectedFlag(s) + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateFilterViewToken(runtime) + body := buildConditionBody(runtime, false) + return common.NewDryRunAPI(). + PUT("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/filter_views/:filter_view_id/conditions/:condition_id"). + Body(body).Set("token", token).Set("sheet_id", runtime.Str("sheet-id")). + Set("filter_view_id", runtime.Str("filter-view-id")).Set("condition_id", runtime.Str("condition-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateFilterViewToken(runtime) + body := buildConditionBody(runtime, false) + data, err := runtime.CallAPI("PUT", + filterViewConditionItemPath(token, runtime.Str("sheet-id"), runtime.Str("filter-view-id"), runtime.Str("condition-id")), + nil, body) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +var SheetListFilterViewConditions = common.Shortcut{ + Service: "sheets", + Command: "+list-filter-view-conditions", + Description: "List all filter conditions of a filter view", + Risk: "read", + Scopes: []string{"sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL (required if --spreadsheet-token is not set)"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token (required if --url is not set)"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "filter-view-id", Desc: "filter view ID", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + _, err := validateFilterViewToken(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateFilterViewToken(runtime) + return common.NewDryRunAPI(). + GET("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/filter_views/:filter_view_id/conditions/query"). + Set("token", token).Set("sheet_id", runtime.Str("sheet-id")).Set("filter_view_id", runtime.Str("filter-view-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateFilterViewToken(runtime) + data, err := runtime.CallAPI("GET", + filterViewConditionBasePath(token, runtime.Str("sheet-id"), runtime.Str("filter-view-id"))+"/query", + nil, nil) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +var SheetGetFilterViewCondition = common.Shortcut{ + Service: "sheets", + Command: "+get-filter-view-condition", + Description: "Get a filter condition by column", + Risk: "read", + Scopes: []string{"sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL (required if --spreadsheet-token is not set)"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token (required if --url is not set)"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "filter-view-id", Desc: "filter view ID", Required: true}, + {Name: "condition-id", Desc: "column letter (e.g. E)", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + _, err := validateFilterViewToken(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateFilterViewToken(runtime) + return common.NewDryRunAPI(). + GET("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/filter_views/:filter_view_id/conditions/:condition_id"). + Set("token", token).Set("sheet_id", runtime.Str("sheet-id")). + Set("filter_view_id", runtime.Str("filter-view-id")).Set("condition_id", runtime.Str("condition-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateFilterViewToken(runtime) + data, err := runtime.CallAPI("GET", + filterViewConditionItemPath(token, runtime.Str("sheet-id"), runtime.Str("filter-view-id"), runtime.Str("condition-id")), + nil, nil) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +var SheetDeleteFilterViewCondition = common.Shortcut{ + Service: "sheets", + Command: "+delete-filter-view-condition", + Description: "Delete a filter condition from a filter view", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL (required if --spreadsheet-token is not set)"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token (required if --url is not set)"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "filter-view-id", Desc: "filter view ID", Required: true}, + {Name: "condition-id", Desc: "column letter (e.g. E)", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + _, err := validateFilterViewToken(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := validateFilterViewToken(runtime) + return common.NewDryRunAPI(). + DELETE("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/filter_views/:filter_view_id/conditions/:condition_id"). + Set("token", token).Set("sheet_id", runtime.Str("sheet-id")). + Set("filter_view_id", runtime.Str("filter-view-id")).Set("condition_id", runtime.Str("condition-id")) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, _ := validateFilterViewToken(runtime) + data, err := runtime.CallAPI("DELETE", + filterViewConditionItemPath(token, runtime.Str("sheet-id"), runtime.Str("filter-view-id"), runtime.Str("condition-id")), + nil, nil) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} + +// validateExpectedFlag checks that --expected is a valid JSON array. +func validateExpectedFlag(s string) error { + if s == "" { + return nil + } + var arr []interface{} + if err := json.Unmarshal([]byte(s), &arr); err != nil { + return fmt.Errorf("--expected must be a JSON array (e.g. [\"6\"]), got: %s", s) + } + return nil +} + +// buildConditionBody constructs the request body for condition create/update. +func buildConditionBody(runtime *common.RuntimeContext, includeConditionID bool) map[string]interface{} { + body := map[string]interface{}{} + if includeConditionID { + body["condition_id"] = runtime.Str("condition-id") + } + if s := runtime.Str("filter-type"); s != "" { + body["filter_type"] = s + } + if s := runtime.Str("compare-type"); s != "" { + body["compare_type"] = s + } + if s := runtime.Str("expected"); s != "" { + var arr []interface{} + // Validate already ensures this is a valid JSON array. + _ = json.Unmarshal([]byte(s), &arr) + body["expected"] = arr + } + return body +} diff --git a/shortcuts/sheets/sheet_filter_view_test.go b/shortcuts/sheets/sheet_filter_view_test.go new file mode 100644 index 00000000..c03a7d86 --- /dev/null +++ b/shortcuts/sheets/sheet_filter_view_test.go @@ -0,0 +1,628 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/internal/httpmock" +) + +// ── CreateFilterView ───────────────────────────────────────────────────────── + +func TestCreateFilterViewValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", "sheet-id": "s1", "range": "s1!A1:H14", + "filter-view-name": "", "filter-view-id": "", + }, nil) + err := SheetCreateFilterView.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestCreateFilterViewValidateSuccess(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "sheet-id": "s1", "range": "s1!A1:H14", + "filter-view-name": "", "filter-view-id": "", + }, nil) + if err := SheetCreateFilterView.Validate(context.Background(), rt); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestCreateFilterViewDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", "range": "sheet1!A1:H14", + "filter-view-name": "my view", "filter-view-id": "", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetCreateFilterView.DryRun(context.Background(), rt)) + if !strings.Contains(got, `filter_views`) { + t.Fatalf("DryRun URL missing filter_views: %s", got) + } + if !strings.Contains(got, `"filter_view_name":"my view"`) { + t.Fatalf("DryRun missing name: %s", got) + } +} + +func TestCreateFilterViewExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "filter_view": map[string]interface{}{"filter_view_id": "pH9hbVcCXA", "range": "sheet1!A1:H14"}, + }}, + }) + err := mountAndRunSheets(t, SheetCreateFilterView, []string{ + "+create-filter-view", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--range", "sheet1!A1:H14", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "filter_view_id") { + t.Fatalf("stdout missing filter_view_id: %s", stdout.String()) + } +} + +func TestCreateFilterViewExecuteAPIError(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views", + Status: 400, Body: map[string]interface{}{"code": 90001, "msg": "invalid"}, + }) + err := mountAndRunSheets(t, SheetCreateFilterView, []string{ + "+create-filter-view", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--range", "sheet1!A1:H14", "--as", "user", + }, f, nil) + if err == nil { + t.Fatal("expected error") + } +} + +// ── UpdateFilterView ───────────────────────────────────────────────────────── + +func TestUpdateFilterViewDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", + "filter-view-id": "pH9hbVcCXA", "range": "sheet1!A1:J20", "filter-view-name": "", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetUpdateFilterView.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"PATCH"`) { + t.Fatalf("DryRun should use PATCH: %s", got) + } + if !strings.Contains(got, `pH9hbVcCXA`) { + t.Fatalf("DryRun missing filter_view_id: %s", got) + } +} + +func TestUpdateFilterViewExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "PATCH", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views/fv123", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "filter_view": map[string]interface{}{"filter_view_id": "fv123", "range": "sheet1!A1:J20"}, + }}, + }) + err := mountAndRunSheets(t, SheetUpdateFilterView, []string{ + "+update-filter-view", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv123", "--range", "sheet1!A1:J20", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestUpdateFilterViewRejectsNoFields(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, sheetsTestConfig()) + err := mountAndRunSheets(t, SheetUpdateFilterView, []string{ + "+update-filter-view", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", + "--as", "user", + }, f, stdout) + if err == nil { + t.Fatal("expected validation error when no update fields provided, got nil") + } + if !strings.Contains(err.Error(), "at least one") { + t.Fatalf("unexpected error message: %v", err) + } +} + +// ── ListFilterViews ────────────────────────────────────────────────────────── + +func TestListFilterViewsDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetListFilterViews.DryRun(context.Background(), rt)) + if !strings.Contains(got, `filter_views/query`) { + t.Fatalf("DryRun URL missing query: %s", got) + } +} + +func TestListFilterViewsExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views/query", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "items": []interface{}{map[string]interface{}{"filter_view_id": "fv1"}}, + }}, + }) + err := mountAndRunSheets(t, SheetListFilterViews, []string{ + "+list-filter-views", "--spreadsheet-token", "shtTOKEN", "--sheet-id", "sheet1", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "fv1") { + t.Fatalf("stdout missing fv1: %s", stdout.String()) + } +} + +// ── GetFilterView ──────────────────────────────────────────────────────────── + +func TestGetFilterViewDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", "filter-view-id": "fv123", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetGetFilterView.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"GET"`) { + t.Fatalf("DryRun should use GET: %s", got) + } + if !strings.Contains(got, `fv123`) { + t.Fatalf("DryRun missing filter_view_id: %s", got) + } +} + +func TestGetFilterViewExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views/fv123", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "filter_view": map[string]interface{}{"filter_view_id": "fv123"}, + }}, + }) + err := mountAndRunSheets(t, SheetGetFilterView, []string{ + "+get-filter-view", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv123", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// ── DeleteFilterView ───────────────────────────────────────────────────────── + +func TestDeleteFilterViewDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", "filter-view-id": "fv123", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetDeleteFilterView.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"DELETE"`) { + t.Fatalf("DryRun should use DELETE: %s", got) + } +} + +func TestDeleteFilterViewExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "DELETE", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views/fv123", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{}}, + }) + err := mountAndRunSheets(t, SheetDeleteFilterView, []string{ + "+delete-filter-view", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv123", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// ── CreateFilterViewCondition ──────────────────────────────────────────────── + +func TestCreateFilterViewConditionValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", "sheet-id": "s1", "filter-view-id": "fv1", + "condition-id": "E", "filter-type": "number", "compare-type": "less", "expected": `["6"]`, + }, nil) + err := SheetCreateFilterViewCondition.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestCreateFilterViewConditionDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", "filter-view-id": "fv1", + "condition-id": "E", "filter-type": "number", "compare-type": "less", "expected": `["6"]`, + }, nil) + got := mustMarshalSheetsDryRun(t, SheetCreateFilterViewCondition.DryRun(context.Background(), rt)) + if !strings.Contains(got, `conditions`) { + t.Fatalf("DryRun URL missing conditions: %s", got) + } + if !strings.Contains(got, `"condition_id":"E"`) { + t.Fatalf("DryRun missing condition_id: %s", got) + } + if !strings.Contains(got, `"filter_type":"number"`) { + t.Fatalf("DryRun missing filter_type: %s", got) + } +} + +func TestCreateFilterViewConditionExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + stub := &httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views/fv1/conditions", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "condition": map[string]interface{}{"condition_id": "E", "filter_type": "number"}, + }}, + } + reg.Register(stub) + err := mountAndRunSheets(t, SheetCreateFilterViewCondition, []string{ + "+create-filter-view-condition", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", + "--condition-id", "E", "--filter-type", "number", "--compare-type", "less", + "--expected", `["6"]`, "--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) + } + if body["condition_id"] != "E" { + t.Fatalf("unexpected condition_id: %v", body["condition_id"]) + } +} + +// ── UpdateFilterViewCondition ──────────────────────────────────────────────── + +func TestUpdateFilterViewConditionDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", "filter-view-id": "fv1", + "condition-id": "E", "filter-type": "number", "compare-type": "between", "expected": `["2","10"]`, + }, nil) + got := mustMarshalSheetsDryRun(t, SheetUpdateFilterViewCondition.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"PUT"`) { + t.Fatalf("DryRun should use PUT: %s", got) + } + if !strings.Contains(got, `"compare_type":"between"`) { + t.Fatalf("DryRun missing compare_type: %s", got) + } +} + +func TestUpdateFilterViewConditionExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "PUT", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views/fv1/conditions/E", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "condition": map[string]interface{}{"condition_id": "E"}, + }}, + }) + err := mountAndRunSheets(t, SheetUpdateFilterViewCondition, []string{ + "+update-filter-view-condition", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--condition-id", "E", + "--filter-type", "number", "--compare-type", "between", "--expected", `["2","10"]`, "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestUpdateFilterViewConditionRejectsNoFields(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, sheetsTestConfig()) + err := mountAndRunSheets(t, SheetUpdateFilterViewCondition, []string{ + "+update-filter-view-condition", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--condition-id", "E", + "--as", "user", + }, f, stdout) + if err == nil { + t.Fatal("expected validation error when no update fields provided, got nil") + } + if !strings.Contains(err.Error(), "at least one") { + t.Fatalf("unexpected error message: %v", err) + } +} + +// ── ListFilterViewConditions ───────────────────────────────────────────────── + +func TestListFilterViewConditionsDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", "filter-view-id": "fv1", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetListFilterViewConditions.DryRun(context.Background(), rt)) + if !strings.Contains(got, `conditions/query`) { + t.Fatalf("DryRun URL missing conditions/query: %s", got) + } +} + +func TestListFilterViewConditionsExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views/fv1/conditions/query", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "items": []interface{}{map[string]interface{}{"condition_id": "E"}}, + }}, + }) + err := mountAndRunSheets(t, SheetListFilterViewConditions, []string{ + "+list-filter-view-conditions", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// ── GetFilterViewCondition ─────────────────────────────────────────────────── + +func TestGetFilterViewConditionDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", + "filter-view-id": "fv1", "condition-id": "E", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetGetFilterViewCondition.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"GET"`) { + t.Fatalf("DryRun should use GET: %s", got) + } +} + +func TestGetFilterViewConditionExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views/fv1/conditions/E", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "condition": map[string]interface{}{"condition_id": "E", "filter_type": "number"}, + }}, + }) + err := mountAndRunSheets(t, SheetGetFilterViewCondition, []string{ + "+get-filter-view-condition", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--condition-id", "E", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// ── DeleteFilterViewCondition ──────────────────────────────────────────────── + +func TestDeleteFilterViewConditionDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", + "filter-view-id": "fv1", "condition-id": "E", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetDeleteFilterViewCondition.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"DELETE"`) { + t.Fatalf("DryRun should use DELETE: %s", got) + } +} + +func TestDeleteFilterViewConditionExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "DELETE", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/filter_views/fv1/conditions/E", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{}}, + }) + err := mountAndRunSheets(t, SheetDeleteFilterViewCondition, []string{ + "+delete-filter-view-condition", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--condition-id", "E", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// ── URL flag coverage ──────────────────────────────────────────────────────── + +func TestCreateFilterViewWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v3/spreadsheets/shtFromURL/sheets/sheet1/filter_views", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "filter_view": map[string]interface{}{"filter_view_id": "fv1"}, + }}, + }) + err := mountAndRunSheets(t, SheetCreateFilterView, []string{ + "+create-filter-view", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--range", "sheet1!A1:H14", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestListFilterViewsWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", URL: "/open-apis/sheets/v3/spreadsheets/shtFromURL/sheets/sheet1/filter_views/query", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{"items": []interface{}{}}}, + }) + err := mountAndRunSheets(t, SheetListFilterViews, []string{ + "+list-filter-views", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGetFilterViewWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", URL: "/open-apis/sheets/v3/spreadsheets/shtFromURL/sheets/sheet1/filter_views/fv1", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "filter_view": map[string]interface{}{"filter_view_id": "fv1"}, + }}, + }) + err := mountAndRunSheets(t, SheetGetFilterView, []string{ + "+get-filter-view", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestUpdateFilterViewWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "PATCH", URL: "/open-apis/sheets/v3/spreadsheets/shtFromURL/sheets/sheet1/filter_views/fv1", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "filter_view": map[string]interface{}{"filter_view_id": "fv1"}, + }}, + }) + err := mountAndRunSheets(t, SheetUpdateFilterView, []string{ + "+update-filter-view", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--range", "sheet1!A1:J20", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestDeleteFilterViewWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "DELETE", URL: "/open-apis/sheets/v3/spreadsheets/shtFromURL/sheets/sheet1/filter_views/fv1", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{}}, + }) + err := mountAndRunSheets(t, SheetDeleteFilterView, []string{ + "+delete-filter-view", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestCreateFilterViewConditionWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v3/spreadsheets/shtFromURL/sheets/sheet1/filter_views/fv1/conditions", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "condition": map[string]interface{}{"condition_id": "E"}, + }}, + }) + err := mountAndRunSheets(t, SheetCreateFilterViewCondition, []string{ + "+create-filter-view-condition", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", + "--condition-id", "E", "--filter-type", "number", "--compare-type", "less", + "--expected", `["6"]`, "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestUpdateFilterViewConditionWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "PUT", URL: "/open-apis/sheets/v3/spreadsheets/shtFromURL/sheets/sheet1/filter_views/fv1/conditions/E", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "condition": map[string]interface{}{"condition_id": "E"}, + }}, + }) + err := mountAndRunSheets(t, SheetUpdateFilterViewCondition, []string{ + "+update-filter-view-condition", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--condition-id", "E", + "--filter-type", "number", "--expected", `["5"]`, "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestListFilterViewConditionsWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", URL: "/open-apis/sheets/v3/spreadsheets/shtFromURL/sheets/sheet1/filter_views/fv1/conditions/query", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{"items": []interface{}{}}}, + }) + err := mountAndRunSheets(t, SheetListFilterViewConditions, []string{ + "+list-filter-view-conditions", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGetFilterViewConditionWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", URL: "/open-apis/sheets/v3/spreadsheets/shtFromURL/sheets/sheet1/filter_views/fv1/conditions/E", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "condition": map[string]interface{}{"condition_id": "E"}, + }}, + }) + err := mountAndRunSheets(t, SheetGetFilterViewCondition, []string{ + "+get-filter-view-condition", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--condition-id", "E", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestDeleteFilterViewConditionWithURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "DELETE", URL: "/open-apis/sheets/v3/spreadsheets/shtFromURL/sheets/sheet1/filter_views/fv1/conditions/E", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{}}, + }) + err := mountAndRunSheets(t, SheetDeleteFilterViewCondition, []string{ + "+delete-filter-view-condition", "--url", "https://example.feishu.cn/sheets/shtFromURL", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", "--condition-id", "E", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// ── --expected validation rejects non-array input ──────────────────────────── + +func TestCreateFilterViewConditionRejectsNonArrayExpected(t *testing.T) { + cases := []struct { + name string + expected string + }{ + {"plain string", "hello"}, + {"JSON object", `{"key":"val"}`}, + {"JSON number", "42"}, + {"JSON string", `"hello"`}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, sheetsTestConfig()) + err := mountAndRunSheets(t, SheetCreateFilterViewCondition, []string{ + "+create-filter-view-condition", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--filter-view-id", "fv1", + "--condition-id", "A", "--filter-type", "text", "--compare-type", "contains", + "--expected", tc.expected, "--as", "user", + }, f, stdout) + if err == nil { + t.Fatalf("expected validation error for --expected=%q, got nil", tc.expected) + } + if !strings.Contains(err.Error(), "--expected must be a JSON array") { + t.Fatalf("unexpected error message: %v", err) + } + }) + } +} diff --git a/shortcuts/sheets/shortcuts.go b/shortcuts/sheets/shortcuts.go index 3a5cc5ac..65bffb3d 100644 --- a/shortcuts/sheets/shortcuts.go +++ b/shortcuts/sheets/shortcuts.go @@ -26,5 +26,15 @@ func Shortcuts() []common.Shortcut { SheetUpdateDimension, SheetMoveDimension, SheetDeleteDimension, + SheetCreateFilterView, + SheetUpdateFilterView, + SheetListFilterViews, + SheetGetFilterView, + SheetDeleteFilterView, + SheetCreateFilterViewCondition, + SheetUpdateFilterViewCondition, + SheetListFilterViewConditions, + SheetGetFilterViewCondition, + SheetDeleteFilterViewCondition, } } diff --git a/skills/lark-sheets/SKILL.md b/skills/lark-sheets/SKILL.md index 6a0c44d3..408939f7 100644 --- a/skills/lark-sheets/SKILL.md +++ b/skills/lark-sheets/SKILL.md @@ -165,6 +165,16 @@ Shortcut 是对常用操作的高级封装(`lark-cli sheets + [flags]` | [`+update-dimension`](references/lark-sheets-update-dimension.md) | Update row or column properties (visibility, size) | | [`+move-dimension`](references/lark-sheets-move-dimension.md) | Move rows or columns to a new position | | [`+delete-dimension`](references/lark-sheets-delete-dimension.md) | Delete rows or columns | +| [`+create-filter-view`](references/lark-sheets-create-filter-view.md) | Create a filter view | +| [`+update-filter-view`](references/lark-sheets-update-filter-view.md) | Update a filter view | +| [`+list-filter-views`](references/lark-sheets-list-filter-views.md) | List all filter views in a sheet | +| [`+get-filter-view`](references/lark-sheets-get-filter-view.md) | Get a filter view by ID | +| [`+delete-filter-view`](references/lark-sheets-delete-filter-view.md) | Delete a filter view | +| [`+create-filter-view-condition`](references/lark-sheets-create-filter-view-condition.md) | Create a filter condition on a filter view | +| [`+update-filter-view-condition`](references/lark-sheets-update-filter-view-condition.md) | Update a filter condition | +| [`+list-filter-view-conditions`](references/lark-sheets-list-filter-view-conditions.md) | List all filter conditions of a filter view | +| [`+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 | ## API Resources diff --git a/skills/lark-sheets/references/lark-sheets-create-filter-view-condition.md b/skills/lark-sheets/references/lark-sheets-create-filter-view-condition.md new file mode 100644 index 00000000..48f3f7ef --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-create-filter-view-condition.md @@ -0,0 +1,42 @@ + +# sheets +create-filter-view-condition(创建筛选条件) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +create-filter-view-condition`。 + +为筛选视图的指定列创建筛选条件。 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。 + +## 命令 + +```bash +# 数值筛选:E 列 < 6 +lark-cli sheets +create-filter-view-condition --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --filter-view-id "" \ + --condition-id "E" --filter-type "number" --compare-type "less" --expected '["6"]' + +# 文本筛选:G 列以 a 开头 +lark-cli sheets +create-filter-view-condition --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --filter-view-id "" \ + --condition-id "G" --filter-type "text" --compare-type "beginsWith" --expected '["a"]' +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | +| `--filter-view-id` | 是 | 筛选视图 ID | +| `--condition-id` | 是 | 列字母(如 `E`) | +| `--filter-type` | 是 | 筛选类型:`hiddenValue`、`number`、`text`、`color` | +| `--compare-type` | 否 | 比较运算符(如 `less`、`beginsWith`、`between`) | +| `--expected` | 是 | 筛选值 JSON 数组(如 `["6"]` 或 `["2","10"]`) | + +## 输出 + +JSON,包含 `condition`(condition_id, filter_type, compare_type, expected)。 diff --git a/skills/lark-sheets/references/lark-sheets-create-filter-view.md b/skills/lark-sheets/references/lark-sheets-create-filter-view.md new file mode 100644 index 00000000..4d60f34f --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-create-filter-view.md @@ -0,0 +1,42 @@ + +# sheets +create-filter-view(创建筛选视图) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +create-filter-view`。 + +在工作表中创建筛选视图,每个工作表最多 150 个筛选视图。 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。可以先用 `--dry-run` 预览。 + +## 命令 + +```bash +lark-cli sheets +create-filter-view --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --range "!A1:H14" + +# 指定名称 +lark-cli sheets +create-filter-view --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --range "!A1:H14" --filter-view-name "我的筛选" +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | +| `--range` | 是 | 筛选范围(如 `sheetId!A1:H14`) | +| `--filter-view-name` | 否 | 显示名称(最多 100 字符) | +| `--filter-view-id` | 否 | 自定义 10 位字母数字 ID(不传则自动生成) | + +## 输出 + +JSON,包含 `filter_view`(filter_view_id, filter_view_name, range)。 + +## 参考 + +- [lark-sheets-list-filter-views](lark-sheets-list-filter-views.md) — 查询所有筛选视图 +- [lark-sheets-create-filter-view-condition](lark-sheets-create-filter-view-condition.md) — 添加筛选条件 diff --git a/skills/lark-sheets/references/lark-sheets-delete-filter-view-condition.md b/skills/lark-sheets/references/lark-sheets-delete-filter-view-condition.md new file mode 100644 index 00000000..619c5ae9 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-delete-filter-view-condition.md @@ -0,0 +1,26 @@ + +# sheets +delete-filter-view-condition(删除筛选条件) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +delete-filter-view-condition`。 + +> [!CAUTION] +> 这是**破坏性写入操作** —— 删除后不可恢复。 + +## 命令 + +```bash +lark-cli sheets +delete-filter-view-condition --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --filter-view-id "" --condition-id "E" +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | +| `--filter-view-id` | 是 | 筛选视图 ID | +| `--condition-id` | 是 | 列字母(如 `E`) | diff --git a/skills/lark-sheets/references/lark-sheets-delete-filter-view.md b/skills/lark-sheets/references/lark-sheets-delete-filter-view.md new file mode 100644 index 00000000..fa6b706b --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-delete-filter-view.md @@ -0,0 +1,25 @@ + +# sheets +delete-filter-view(删除筛选视图) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +delete-filter-view`。 + +> [!CAUTION] +> 这是**破坏性写入操作** —— 删除后不可恢复。执行前必须确认用户意图。 + +## 命令 + +```bash +lark-cli sheets +delete-filter-view --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --filter-view-id "" +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | +| `--filter-view-id` | 是 | 筛选视图 ID | diff --git a/skills/lark-sheets/references/lark-sheets-get-filter-view-condition.md b/skills/lark-sheets/references/lark-sheets-get-filter-view-condition.md new file mode 100644 index 00000000..b9d06cae --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-get-filter-view-condition.md @@ -0,0 +1,27 @@ + +# sheets +get-filter-view-condition(获取筛选条件) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +get-filter-view-condition`。 + +## 命令 + +```bash +lark-cli sheets +get-filter-view-condition --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --filter-view-id "" --condition-id "E" +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | +| `--filter-view-id` | 是 | 筛选视图 ID | +| `--condition-id` | 是 | 列字母(如 `E`) | + +## 输出 + +JSON,包含 `condition`(condition_id, filter_type, compare_type, expected)。 diff --git a/skills/lark-sheets/references/lark-sheets-get-filter-view.md b/skills/lark-sheets/references/lark-sheets-get-filter-view.md new file mode 100644 index 00000000..cf5ff021 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-get-filter-view.md @@ -0,0 +1,26 @@ + +# sheets +get-filter-view(获取筛选视图) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +get-filter-view`。 + +## 命令 + +```bash +lark-cli sheets +get-filter-view --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --filter-view-id "" +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | +| `--filter-view-id` | 是 | 筛选视图 ID | + +## 输出 + +JSON,包含 `filter_view`(filter_view_id, filter_view_name, range)。 diff --git a/skills/lark-sheets/references/lark-sheets-list-filter-view-conditions.md b/skills/lark-sheets/references/lark-sheets-list-filter-view-conditions.md new file mode 100644 index 00000000..569c514c --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-list-filter-view-conditions.md @@ -0,0 +1,28 @@ + +# sheets +list-filter-view-conditions(查询筛选条件) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +list-filter-view-conditions`。 + +查询筛选视图的所有筛选条件。 + +## 命令 + +```bash +lark-cli sheets +list-filter-view-conditions --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --filter-view-id "" +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | +| `--filter-view-id` | 是 | 筛选视图 ID | + +## 输出 + +JSON,包含 `items[]`(condition_id, filter_type, compare_type, expected)。 diff --git a/skills/lark-sheets/references/lark-sheets-list-filter-views.md b/skills/lark-sheets/references/lark-sheets-list-filter-views.md new file mode 100644 index 00000000..3251fd68 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-list-filter-views.md @@ -0,0 +1,26 @@ + +# sheets +list-filter-views(查询筛选视图) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +list-filter-views`。 + +查询工作表中的所有筛选视图,返回视图 ID、名称和范围。 + +## 命令 + +```bash +lark-cli sheets +list-filter-views --spreadsheet-token "shtxxxxxxxx" --sheet-id "" +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | + +## 输出 + +JSON,包含 `items[]`(filter_view_id, filter_view_name, range)。 diff --git a/skills/lark-sheets/references/lark-sheets-update-filter-view-condition.md b/skills/lark-sheets/references/lark-sheets-update-filter-view-condition.md new file mode 100644 index 00000000..e874e6e9 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-update-filter-view-condition.md @@ -0,0 +1,27 @@ + +# sheets +update-filter-view-condition(更新筛选条件) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +update-filter-view-condition`。 + +## 命令 + +```bash +lark-cli sheets +update-filter-view-condition --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --filter-view-id "" --condition-id "E" \ + --filter-type "number" --compare-type "between" --expected '["2","10"]' +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | +| `--filter-view-id` | 是 | 筛选视图 ID | +| `--condition-id` | 是 | 列字母(如 `E`) | +| `--filter-type` | 否 | 筛选类型 | +| `--compare-type` | 否 | 比较运算符 | +| `--expected` | 否 | 筛选值 JSON 数组 | diff --git a/skills/lark-sheets/references/lark-sheets-update-filter-view.md b/skills/lark-sheets/references/lark-sheets-update-filter-view.md new file mode 100644 index 00000000..44858f83 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-update-filter-view.md @@ -0,0 +1,24 @@ + +# sheets +update-filter-view(更新筛选视图) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +update-filter-view`。 + +## 命令 + +```bash +lark-cli sheets +update-filter-view --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --filter-view-id "" --range "!A1:J20" +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url` | 否 | 电子表格 URL | +| `--spreadsheet-token` | 否 | 表格 token | +| `--sheet-id` | 是 | 工作表 ID | +| `--filter-view-id` | 是 | 筛选视图 ID | +| `--range` | 否 | 新的筛选范围 | +| `--filter-view-name` | 否 | 新的显示名称 |