diff --git a/README.md b/README.md index 244ce74..abadb5f 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,12 @@ Add `--format text` for human-readable output, or `--format yaml` for YAML. | `gws sheets copy-to ` | Copy sheet to another spreadsheet (`--sheet-id`, `--destination`) | | `gws sheets batch-read ` | Read multiple ranges (`--ranges`, `--value-render`) | | `gws sheets batch-write ` | Write multiple ranges (`--ranges`, `--values`, `--value-input`) | +| `gws sheets add-named-range ` | Add named range (`--name`) | +| `gws sheets list-named-ranges ` | List all named ranges | +| `gws sheets delete-named-range ` | Delete named range (`--named-range-id`) | +| `gws sheets add-filter ` | Set basic filter on range | +| `gws sheets clear-filter ` | Clear basic filter (`--sheet`) | +| `gws sheets add-filter-view ` | Add filter view (`--name`) | ### Slides diff --git a/cmd/commands_test.go b/cmd/commands_test.go index 23a4ecc..b31566e 100644 --- a/cmd/commands_test.go +++ b/cmd/commands_test.go @@ -402,6 +402,12 @@ func TestSheetsCommands(t *testing.T) { {"copy-to"}, {"batch-read"}, {"batch-write"}, + {"add-named-range"}, + {"list-named-ranges"}, + {"delete-named-range"}, + {"add-filter"}, + {"clear-filter"}, + {"add-filter-view"}, } for _, tt := range tests { diff --git a/cmd/sheets.go b/cmd/sheets.go index b21374b..2d84d76 100644 --- a/cmd/sheets.go +++ b/cmd/sheets.go @@ -295,6 +295,54 @@ Example: RunE: runSheetsBatchWrite, } +var sheetsAddNamedRangeCmd = &cobra.Command{ + Use: "add-named-range ", + Short: "Add a named range", + Long: "Adds a named range to a spreadsheet.", + Args: cobra.ExactArgs(2), + RunE: runSheetsAddNamedRange, +} + +var sheetsListNamedRangesCmd = &cobra.Command{ + Use: "list-named-ranges ", + Short: "List named ranges", + Long: "Lists all named ranges in a spreadsheet.", + Args: cobra.ExactArgs(1), + RunE: runSheetsListNamedRanges, +} + +var sheetsDeleteNamedRangeCmd = &cobra.Command{ + Use: "delete-named-range ", + Short: "Delete a named range", + Long: "Deletes a named range from a spreadsheet.", + Args: cobra.ExactArgs(1), + RunE: runSheetsDeleteNamedRange, +} + +var sheetsAddFilterCmd = &cobra.Command{ + Use: "add-filter ", + Short: "Add a basic filter", + Long: "Sets a basic filter on a range in a spreadsheet.", + Args: cobra.ExactArgs(2), + RunE: runSheetsAddFilter, +} + +var sheetsClearFilterCmd = &cobra.Command{ + Use: "clear-filter ", + Short: "Clear a basic filter", + Long: "Clears the basic filter from a sheet.", + Args: cobra.ExactArgs(1), + RunE: runSheetsClearFilter, +} + +var sheetsAddFilterViewCmd = &cobra.Command{ + Use: "add-filter-view ", + Short: "Add a filter view", + Long: "Creates a new filter view for a range in a spreadsheet.", + Args: cobra.ExactArgs(2), + RunE: runSheetsAddFilterView, +} + func init() { rootCmd.AddCommand(sheetsCmd) sheetsCmd.AddCommand(sheetsInfoCmd) @@ -448,6 +496,28 @@ func init() { sheetsBatchWriteCmd.Flags().String("value-input", "USER_ENTERED", "Value input option: RAW, USER_ENTERED") sheetsBatchWriteCmd.MarkFlagRequired("ranges") sheetsBatchWriteCmd.MarkFlagRequired("values") + + // Named range commands + sheetsCmd.AddCommand(sheetsAddNamedRangeCmd) + sheetsAddNamedRangeCmd.Flags().String("name", "", "Name for the named range (required)") + sheetsAddNamedRangeCmd.MarkFlagRequired("name") + + sheetsCmd.AddCommand(sheetsListNamedRangesCmd) + + sheetsCmd.AddCommand(sheetsDeleteNamedRangeCmd) + sheetsDeleteNamedRangeCmd.Flags().String("named-range-id", "", "ID of the named range to delete (required)") + sheetsDeleteNamedRangeCmd.MarkFlagRequired("named-range-id") + + // Filter commands + sheetsCmd.AddCommand(sheetsAddFilterCmd) + + sheetsCmd.AddCommand(sheetsClearFilterCmd) + sheetsClearFilterCmd.Flags().String("sheet", "", "Sheet name (required)") + sheetsClearFilterCmd.MarkFlagRequired("sheet") + + sheetsCmd.AddCommand(sheetsAddFilterViewCmd) + sheetsAddFilterViewCmd.Flags().String("name", "", "Title for the filter view (required)") + sheetsAddFilterViewCmd.MarkFlagRequired("name") } func runSheetsInfo(cmd *cobra.Command, args []string) error { @@ -2121,3 +2191,287 @@ func runSheetsBatchWrite(cmd *cobra.Command, args []string) error { "cells_updated": resp.TotalUpdatedCells, }) } + +func runSheetsAddNamedRange(cmd *cobra.Command, args []string) error { + p := printer.New(os.Stdout, GetFormat()) + ctx := context.Background() + + factory, err := client.NewFactory(ctx) + if err != nil { + return p.PrintError(err) + } + + svc, err := factory.Sheets() + if err != nil { + return p.PrintError(err) + } + + spreadsheetID := args[0] + rangeStr := args[1] + name, _ := cmd.Flags().GetString("name") + + _, gridRange, err := parseRange(svc, spreadsheetID, rangeStr) + if err != nil { + return p.PrintError(err) + } + + requests := []*sheets.Request{ + { + AddNamedRange: &sheets.AddNamedRangeRequest{ + NamedRange: &sheets.NamedRange{ + Name: name, + Range: gridRange, + }, + }, + }, + } + + resp, err := svc.Spreadsheets.BatchUpdate(spreadsheetID, &sheets.BatchUpdateSpreadsheetRequest{ + Requests: requests, + }).Do() + if err != nil { + return p.PrintError(fmt.Errorf("failed to add named range: %w", err)) + } + + var namedRangeID string + if len(resp.Replies) > 0 && resp.Replies[0].AddNamedRange != nil && resp.Replies[0].AddNamedRange.NamedRange != nil { + namedRangeID = resp.Replies[0].AddNamedRange.NamedRange.NamedRangeId + } + + return p.Print(map[string]interface{}{ + "status": "added", + "spreadsheet": spreadsheetID, + "name": name, + "named_range_id": namedRangeID, + "range": rangeStr, + }) +} + +func runSheetsListNamedRanges(cmd *cobra.Command, args []string) error { + p := printer.New(os.Stdout, GetFormat()) + ctx := context.Background() + + factory, err := client.NewFactory(ctx) + if err != nil { + return p.PrintError(err) + } + + svc, err := factory.Sheets() + if err != nil { + return p.PrintError(err) + } + + spreadsheetID := args[0] + + spreadsheet, err := svc.Spreadsheets.Get(spreadsheetID).Fields("namedRanges").Do() + if err != nil { + return p.PrintError(fmt.Errorf("failed to get named ranges: %w", err)) + } + + namedRanges := make([]map[string]interface{}, 0, len(spreadsheet.NamedRanges)) + for _, nr := range spreadsheet.NamedRanges { + entry := map[string]interface{}{ + "name": nr.Name, + "named_range_id": nr.NamedRangeId, + } + if nr.Range != nil { + entry["range"] = map[string]interface{}{ + "sheet_id": nr.Range.SheetId, + "start_row": nr.Range.StartRowIndex, + "end_row": nr.Range.EndRowIndex, + "start_column": nr.Range.StartColumnIndex, + "end_column": nr.Range.EndColumnIndex, + } + } + namedRanges = append(namedRanges, entry) + } + + return p.Print(map[string]interface{}{ + "named_ranges": namedRanges, + "count": len(namedRanges), + }) +} + +func runSheetsDeleteNamedRange(cmd *cobra.Command, args []string) error { + p := printer.New(os.Stdout, GetFormat()) + ctx := context.Background() + + factory, err := client.NewFactory(ctx) + if err != nil { + return p.PrintError(err) + } + + svc, err := factory.Sheets() + if err != nil { + return p.PrintError(err) + } + + spreadsheetID := args[0] + namedRangeID, _ := cmd.Flags().GetString("named-range-id") + + requests := []*sheets.Request{ + { + DeleteNamedRange: &sheets.DeleteNamedRangeRequest{ + NamedRangeId: namedRangeID, + }, + }, + } + + _, err = svc.Spreadsheets.BatchUpdate(spreadsheetID, &sheets.BatchUpdateSpreadsheetRequest{ + Requests: requests, + }).Do() + if err != nil { + return p.PrintError(fmt.Errorf("failed to delete named range: %w", err)) + } + + return p.Print(map[string]interface{}{ + "status": "deleted", + "spreadsheet": spreadsheetID, + "named_range_id": namedRangeID, + }) +} + +func runSheetsAddFilter(cmd *cobra.Command, args []string) error { + p := printer.New(os.Stdout, GetFormat()) + ctx := context.Background() + + factory, err := client.NewFactory(ctx) + if err != nil { + return p.PrintError(err) + } + + svc, err := factory.Sheets() + if err != nil { + return p.PrintError(err) + } + + spreadsheetID := args[0] + rangeStr := args[1] + + _, gridRange, err := parseRange(svc, spreadsheetID, rangeStr) + if err != nil { + return p.PrintError(err) + } + + requests := []*sheets.Request{ + { + SetBasicFilter: &sheets.SetBasicFilterRequest{ + Filter: &sheets.BasicFilter{ + Range: gridRange, + }, + }, + }, + } + + _, err = svc.Spreadsheets.BatchUpdate(spreadsheetID, &sheets.BatchUpdateSpreadsheetRequest{ + Requests: requests, + }).Do() + if err != nil { + return p.PrintError(fmt.Errorf("failed to add filter: %w", err)) + } + + return p.Print(map[string]interface{}{ + "status": "added", + "spreadsheet": spreadsheetID, + "range": rangeStr, + }) +} + +func runSheetsClearFilter(cmd *cobra.Command, args []string) error { + p := printer.New(os.Stdout, GetFormat()) + ctx := context.Background() + + factory, err := client.NewFactory(ctx) + if err != nil { + return p.PrintError(err) + } + + svc, err := factory.Sheets() + if err != nil { + return p.PrintError(err) + } + + spreadsheetID := args[0] + sheetName, _ := cmd.Flags().GetString("sheet") + + sheetID, err := getSheetID(svc, spreadsheetID, sheetName) + if err != nil { + return p.PrintError(err) + } + + requests := []*sheets.Request{ + { + ClearBasicFilter: &sheets.ClearBasicFilterRequest{ + SheetId: sheetID, + }, + }, + } + + _, err = svc.Spreadsheets.BatchUpdate(spreadsheetID, &sheets.BatchUpdateSpreadsheetRequest{ + Requests: requests, + }).Do() + if err != nil { + return p.PrintError(fmt.Errorf("failed to clear filter: %w", err)) + } + + return p.Print(map[string]interface{}{ + "status": "cleared", + "spreadsheet": spreadsheetID, + "sheet": sheetName, + }) +} + +func runSheetsAddFilterView(cmd *cobra.Command, args []string) error { + p := printer.New(os.Stdout, GetFormat()) + ctx := context.Background() + + factory, err := client.NewFactory(ctx) + if err != nil { + return p.PrintError(err) + } + + svc, err := factory.Sheets() + if err != nil { + return p.PrintError(err) + } + + spreadsheetID := args[0] + rangeStr := args[1] + name, _ := cmd.Flags().GetString("name") + + _, gridRange, err := parseRange(svc, spreadsheetID, rangeStr) + if err != nil { + return p.PrintError(err) + } + + requests := []*sheets.Request{ + { + AddFilterView: &sheets.AddFilterViewRequest{ + Filter: &sheets.FilterView{ + Title: name, + Range: gridRange, + }, + }, + }, + } + + resp, err := svc.Spreadsheets.BatchUpdate(spreadsheetID, &sheets.BatchUpdateSpreadsheetRequest{ + Requests: requests, + }).Do() + if err != nil { + return p.PrintError(fmt.Errorf("failed to add filter view: %w", err)) + } + + var filterViewID int64 + if len(resp.Replies) > 0 && resp.Replies[0].AddFilterView != nil && resp.Replies[0].AddFilterView.Filter != nil { + filterViewID = resp.Replies[0].AddFilterView.Filter.FilterViewId + } + + return p.Print(map[string]interface{}{ + "status": "added", + "spreadsheet": spreadsheetID, + "name": name, + "filter_view_id": filterViewID, + "range": rangeStr, + }) +} diff --git a/cmd/sheets_test.go b/cmd/sheets_test.go index 7a0fb2a..0a97c3c 100644 --- a/cmd/sheets_test.go +++ b/cmd/sheets_test.go @@ -1592,6 +1592,401 @@ func TestSheetsBatchWrite_MockServer(t *testing.T) { } } +// TestSheetsAddNamedRangeCommand_Flags tests add-named-range command flags +func TestSheetsAddNamedRangeCommand_Flags(t *testing.T) { + cmd := findSubcommand(sheetsCmd, "add-named-range") + if cmd == nil { + t.Fatal("add-named-range command not found") + } + + expectedFlags := []string{"name"} + for _, flag := range expectedFlags { + if cmd.Flags().Lookup(flag) == nil { + t.Errorf("expected flag '--%s' not found", flag) + } + } +} + +// TestSheetsListNamedRangesCommand tests list-named-ranges command +func TestSheetsListNamedRangesCommand(t *testing.T) { + cmd := findSubcommand(sheetsCmd, "list-named-ranges") + if cmd == nil { + t.Fatal("list-named-ranges command not found") + } + + if cmd.Use != "list-named-ranges " { + t.Errorf("unexpected Use: %s", cmd.Use) + } +} + +// TestSheetsDeleteNamedRangeCommand_Flags tests delete-named-range command flags +func TestSheetsDeleteNamedRangeCommand_Flags(t *testing.T) { + cmd := findSubcommand(sheetsCmd, "delete-named-range") + if cmd == nil { + t.Fatal("delete-named-range command not found") + } + + expectedFlags := []string{"named-range-id"} + for _, flag := range expectedFlags { + if cmd.Flags().Lookup(flag) == nil { + t.Errorf("expected flag '--%s' not found", flag) + } + } +} + +// TestSheetsAddFilterCommand tests add-filter command +func TestSheetsAddFilterCommand(t *testing.T) { + cmd := findSubcommand(sheetsCmd, "add-filter") + if cmd == nil { + t.Fatal("add-filter command not found") + } + + if cmd.Use != "add-filter " { + t.Errorf("unexpected Use: %s", cmd.Use) + } +} + +// TestSheetsClearFilterCommand_Flags tests clear-filter command flags +func TestSheetsClearFilterCommand_Flags(t *testing.T) { + cmd := findSubcommand(sheetsCmd, "clear-filter") + if cmd == nil { + t.Fatal("clear-filter command not found") + } + + expectedFlags := []string{"sheet"} + for _, flag := range expectedFlags { + if cmd.Flags().Lookup(flag) == nil { + t.Errorf("expected flag '--%s' not found", flag) + } + } +} + +// TestSheetsAddFilterViewCommand_Flags tests add-filter-view command flags +func TestSheetsAddFilterViewCommand_Flags(t *testing.T) { + cmd := findSubcommand(sheetsCmd, "add-filter-view") + if cmd == nil { + t.Fatal("add-filter-view command not found") + } + + expectedFlags := []string{"name"} + for _, flag := range expectedFlags { + if cmd.Flags().Lookup(flag) == nil { + t.Errorf("expected flag '--%s' not found", flag) + } + } +} + +// TestSheetsAddNamedRange_MockServer tests add-named-range API integration +func TestSheetsAddNamedRange_MockServer(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + resp := map[string]interface{}{ + "spreadsheetId": "test-id", + "sheets": []map[string]interface{}{ + { + "properties": map[string]interface{}{ + "sheetId": 0, + "title": "Sheet1", + }, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + + if r.Method == "POST" && strings.Contains(r.URL.Path, ":batchUpdate") { + body, _ := io.ReadAll(r.Body) + var req map[string]interface{} + json.Unmarshal(body, &req) + + requests := req["requests"].([]interface{}) + if len(requests) > 0 { + addNamedRange := requests[0].(map[string]interface{})["addNamedRange"] + if addNamedRange != nil { + namedRange := addNamedRange.(map[string]interface{})["namedRange"].(map[string]interface{}) + name := namedRange["name"].(string) + + resp := map[string]interface{}{ + "spreadsheetId": "test-id", + "replies": []map[string]interface{}{ + { + "addNamedRange": map[string]interface{}{ + "namedRange": map[string]interface{}{ + "namedRangeId": "nr-123", + "name": name, + }, + }, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + } + } + http.Error(w, "not found", http.StatusNotFound) + })) + defer server.Close() + + if server == nil { + t.Fatal("server not created") + } +} + +// TestSheetsListNamedRanges_MockServer tests list-named-ranges API integration +func TestSheetsListNamedRanges_MockServer(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && strings.Contains(r.URL.Path, "/spreadsheets/") { + resp := map[string]interface{}{ + "namedRanges": []map[string]interface{}{ + { + "namedRangeId": "nr-123", + "name": "MyRange", + "range": map[string]interface{}{ + "sheetId": 0, + "startRowIndex": 0, + "endRowIndex": 10, + "startColumnIndex": 0, + "endColumnIndex": 4, + }, + }, + { + "namedRangeId": "nr-456", + "name": "DataRange", + "range": map[string]interface{}{ + "sheetId": 0, + "startRowIndex": 0, + "endRowIndex": 100, + "startColumnIndex": 0, + "endColumnIndex": 26, + }, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + http.Error(w, "not found", http.StatusNotFound) + })) + defer server.Close() + + if server == nil { + t.Fatal("server not created") + } +} + +// TestSheetsDeleteNamedRange_MockServer tests delete-named-range API integration +func TestSheetsDeleteNamedRange_MockServer(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" && strings.Contains(r.URL.Path, ":batchUpdate") { + body, _ := io.ReadAll(r.Body) + var req map[string]interface{} + json.Unmarshal(body, &req) + + requests := req["requests"].([]interface{}) + if len(requests) > 0 { + deleteNamedRange := requests[0].(map[string]interface{})["deleteNamedRange"] + if deleteNamedRange != nil { + resp := map[string]interface{}{ + "spreadsheetId": "test-id", + "replies": []map[string]interface{}{{}}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + } + } + http.Error(w, "not found", http.StatusNotFound) + })) + defer server.Close() + + if server == nil { + t.Fatal("server not created") + } +} + +// TestSheetsAddFilter_MockServer tests add-filter API integration +func TestSheetsAddFilter_MockServer(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + resp := map[string]interface{}{ + "spreadsheetId": "test-id", + "sheets": []map[string]interface{}{ + { + "properties": map[string]interface{}{ + "sheetId": 0, + "title": "Sheet1", + }, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + + if r.Method == "POST" && strings.Contains(r.URL.Path, ":batchUpdate") { + body, _ := io.ReadAll(r.Body) + var req map[string]interface{} + json.Unmarshal(body, &req) + + requests := req["requests"].([]interface{}) + if len(requests) > 0 { + setBasicFilter := requests[0].(map[string]interface{})["setBasicFilter"] + if setBasicFilter != nil { + resp := map[string]interface{}{ + "spreadsheetId": "test-id", + "replies": []map[string]interface{}{{}}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + } + } + http.Error(w, "not found", http.StatusNotFound) + })) + defer server.Close() + + if server == nil { + t.Fatal("server not created") + } +} + +// TestSheetsClearFilter_MockServer tests clear-filter API integration +func TestSheetsClearFilter_MockServer(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + resp := map[string]interface{}{ + "spreadsheetId": "test-id", + "sheets": []map[string]interface{}{ + { + "properties": map[string]interface{}{ + "sheetId": 0, + "title": "Sheet1", + }, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + + if r.Method == "POST" && strings.Contains(r.URL.Path, ":batchUpdate") { + body, _ := io.ReadAll(r.Body) + var req map[string]interface{} + json.Unmarshal(body, &req) + + requests := req["requests"].([]interface{}) + if len(requests) > 0 { + clearFilter := requests[0].(map[string]interface{})["clearBasicFilter"] + if clearFilter != nil { + resp := map[string]interface{}{ + "spreadsheetId": "test-id", + "replies": []map[string]interface{}{{}}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + } + } + http.Error(w, "not found", http.StatusNotFound) + })) + defer server.Close() + + if server == nil { + t.Fatal("server not created") + } +} + +// TestSheetsAddFilterView_MockServer tests add-filter-view API integration +func TestSheetsAddFilterView_MockServer(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + resp := map[string]interface{}{ + "spreadsheetId": "test-id", + "sheets": []map[string]interface{}{ + { + "properties": map[string]interface{}{ + "sheetId": 0, + "title": "Sheet1", + }, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + + if r.Method == "POST" && strings.Contains(r.URL.Path, ":batchUpdate") { + body, _ := io.ReadAll(r.Body) + var req map[string]interface{} + json.Unmarshal(body, &req) + + requests := req["requests"].([]interface{}) + if len(requests) > 0 { + addFilterView := requests[0].(map[string]interface{})["addFilterView"] + if addFilterView != nil { + filter := addFilterView.(map[string]interface{})["filter"].(map[string]interface{}) + title := filter["title"].(string) + + resp := map[string]interface{}{ + "spreadsheetId": "test-id", + "replies": []map[string]interface{}{ + { + "addFilterView": map[string]interface{}{ + "filter": map[string]interface{}{ + "filterViewId": 98765, + "title": title, + }, + }, + }, + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + return + } + } + } + http.Error(w, "not found", http.StatusNotFound) + })) + defer server.Close() + + if server == nil { + t.Fatal("server not created") + } +} + +// TestSheetsCommands_Structure_NamedRangesAndFilters tests that named range and filter commands are registered +func TestSheetsCommands_Structure_NamedRangesAndFilters(t *testing.T) { + commands := []string{ + "add-named-range", + "list-named-ranges", + "delete-named-range", + "add-filter", + "clear-filter", + "add-filter-view", + } + + for _, cmdName := range commands { + t.Run(cmdName, func(t *testing.T) { + cmd := findSubcommand(sheetsCmd, cmdName) + if cmd == nil { + t.Fatalf("command '%s' not found", cmdName) + } + }) + } +} + // TestSheetsFreeze_MockServer tests freeze panes API integration func TestSheetsFreeze_MockServer(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/skills/sheets/SKILL.md b/skills/sheets/SKILL.md index aee6cb5..7b7b48e 100644 --- a/skills/sheets/SKILL.md +++ b/skills/sheets/SKILL.md @@ -9,7 +9,7 @@ metadata: # Google Sheets (gws sheets) -`gws sheets` provides CLI access to Google Sheets with structured JSON output. This skill has 22 commands covering full spreadsheet management including batch operations. +`gws sheets` provides CLI access to Google Sheets with structured JSON output. This skill has 32 commands covering full spreadsheet management including batch operations, named ranges, and filters. > **Disclaimer:** `gws` is not the official Google CLI. This is an independent, open-source project not endorsed by or affiliated with Google. @@ -71,6 +71,20 @@ For initial setup, see the `gws-auth` skill. | Insert columns | `gws sheets insert-cols --sheet "Sheet1" --at 2 --count 1` | | Delete columns | `gws sheets delete-cols --sheet "Sheet1" --from 2 --to 4` | +### Named Ranges +| Task | Command | +|------|---------| +| Add a named range | `gws sheets add-named-range "Sheet1!A1:D10" --name "MyRange"` | +| List named ranges | `gws sheets list-named-ranges ` | +| Delete a named range | `gws sheets delete-named-range --named-range-id "nr-123"` | + +### Filters +| Task | Command | +|------|---------| +| Add a basic filter | `gws sheets add-filter "Sheet1!A1:D10"` | +| Clear a basic filter | `gws sheets clear-filter --sheet "Sheet1"` | +| Add a filter view | `gws sheets add-filter-view "Sheet1!A1:D10" --name "My View"` | + ### Cell Operations | Task | Command | |------|---------| @@ -285,6 +299,60 @@ gws sheets batch-write --ranges "A1:B2" --values '[[1,2],[3,4]] The nth `--ranges` pairs with the nth `--values`. +### add-named-range — Add a named range + +```bash +gws sheets add-named-range --name +``` + +**Flags:** +- `--name string` — Name for the named range (required) + +### list-named-ranges — List named ranges + +```bash +gws sheets list-named-ranges +``` + +### delete-named-range — Delete a named range + +```bash +gws sheets delete-named-range --named-range-id +``` + +**Flags:** +- `--named-range-id string` — ID of the named range to delete (required) + +Use `list-named-ranges` to find the IDs. + +### add-filter — Add a basic filter + +```bash +gws sheets add-filter +``` + +Sets a basic filter on the specified range. Only one basic filter per sheet. + +### clear-filter — Clear a basic filter + +```bash +gws sheets clear-filter --sheet +``` + +**Flags:** +- `--sheet string` — Sheet name (required) + +### add-filter-view — Add a filter view + +```bash +gws sheets add-filter-view --name +``` + +**Flags:** +- `--name string` — Title for the filter view (required) + +Filter views are saved views that don't affect other users. + ## Output Modes ```bash diff --git a/skills/sheets/references/commands.md b/skills/sheets/references/commands.md index befbf18..d1047fc 100644 --- a/skills/sheets/references/commands.md +++ b/skills/sheets/references/commands.md @@ -1,6 +1,6 @@ # Sheets Commands Reference -Complete flag and option reference for `gws sheets` commands — 30 commands total. +Complete flag and option reference for `gws sheets` commands — 32 commands total. > **Disclaimer:** `gws` is not the official Google CLI. This is an independent, open-source project not endorsed by or affiliated with Google. @@ -14,7 +14,7 @@ Complete flag and option reference for `gws sheets` commands — 30 commands tot ## Range Format Reference -Ranges are used by `read`, `write`, `append`, `clear`, `merge`, `unmerge`, `sort`, and `format`. +Ranges are used by `read`, `write`, `append`, `clear`, `merge`, `unmerge`, `sort`, `format`, `add-named-range`, `add-filter`, and `add-filter-view`. | Format | Example | Description | |--------|---------|-------------| @@ -587,3 +587,172 @@ gws sheets batch-write 1abc123xyz \ - Number of `--ranges` flags must match number of `--values` flags - Values must be JSON arrays (e.g., `'[["a","b"],["c","d"]]'`) - More efficient than multiple `gws sheets write` calls + +--- + +## gws sheets add-named-range + +Adds a named range to a spreadsheet. + +``` +Usage: gws sheets add-named-range <spreadsheet-id> <range> [flags] +``` + +| Flag | Type | Default | Required | Description | +|------|------|---------|----------|-------------| +| `--name` | string | | Yes | Name for the named range | + +### Examples + +```bash +# Create a named range for a data table +gws sheets add-named-range 1abc123xyz "Sheet1!A1:D100" --name "SalesData" + +# Create a named range in a specific sheet +gws sheets add-named-range 1abc123xyz "Inventory!B2:F50" --name "StockLevels" +``` + +### Notes + +- The response includes the `named_range_id` which is needed for deletion +- Named ranges can be used in formulas (e.g., `=SUM(SalesData)`) +- Range must include both start and end cells (e.g., `A1:D10`) + +--- + +## gws sheets list-named-ranges + +Lists all named ranges in a spreadsheet. + +``` +Usage: gws sheets list-named-ranges <spreadsheet-id> +``` + +No additional flags. + +### Examples + +```bash +# List all named ranges +gws sheets list-named-ranges 1abc123xyz +``` + +### Notes + +- Returns name, `named_range_id`, and range coordinates for each named range +- Use the `named_range_id` with `delete-named-range` to remove a range + +--- + +## gws sheets delete-named-range + +Deletes a named range from a spreadsheet. + +``` +Usage: gws sheets delete-named-range <spreadsheet-id> [flags] +``` + +| Flag | Type | Default | Required | Description | +|------|------|---------|----------|-------------| +| `--named-range-id` | string | | Yes | ID of the named range to delete | + +### Examples + +```bash +# Delete a named range by ID (get IDs from list-named-ranges) +gws sheets delete-named-range 1abc123xyz --named-range-id "nr-abc123" +``` + +### Notes + +- Use `list-named-ranges` to find named range IDs +- Deleting a named range does not delete the underlying data + +--- + +## gws sheets add-filter + +Sets a basic filter on a range in a spreadsheet. + +``` +Usage: gws sheets add-filter <spreadsheet-id> <range> +``` + +No additional flags — the range is positional. + +### Examples + +```bash +# Add a filter to a data range +gws sheets add-filter 1abc123xyz "Sheet1!A1:D100" + +# Add a filter in the first sheet +gws sheets add-filter 1abc123xyz "A1:F50" +``` + +### Notes + +- Only one basic filter is allowed per sheet +- Setting a new filter replaces any existing basic filter on the sheet +- Basic filters add dropdown arrows to the header row for column filtering +- Range must include both start and end cells + +--- + +## gws sheets clear-filter + +Clears the basic filter from a sheet. + +``` +Usage: gws sheets clear-filter <spreadsheet-id> [flags] +``` + +| Flag | Type | Default | Required | Description | +|------|------|---------|----------|-------------| +| `--sheet` | string | | Yes | Sheet name | + +### Examples + +```bash +# Clear the basic filter from Sheet1 +gws sheets clear-filter 1abc123xyz --sheet "Sheet1" + +# Clear filter from a named sheet +gws sheets clear-filter 1abc123xyz --sheet "Data" +``` + +### Notes + +- Only removes the filter — does not affect the underlying data +- If no filter exists on the sheet, this is a no-op + +--- + +## gws sheets add-filter-view + +Creates a new filter view for a range in a spreadsheet. + +``` +Usage: gws sheets add-filter-view <spreadsheet-id> <range> [flags] +``` + +| Flag | Type | Default | Required | Description | +|------|------|---------|----------|-------------| +| `--name` | string | | Yes | Title for the filter view | + +### Examples + +```bash +# Create a filter view +gws sheets add-filter-view 1abc123xyz "Sheet1!A1:D100" --name "Active Items" + +# Create a filter view in the first sheet +gws sheets add-filter-view 1abc123xyz "A1:F50" --name "Q1 Data" +``` + +### Notes + +- Filter views are saved named views that don't affect other users +- Multiple filter views can exist on the same sheet +- The response includes the `filter_view_id` +- Unlike basic filters, filter views are per-user and don't change the shared view