Compare commits
2 Commits
main
...
feature/sh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22cf640a07 | ||
|
|
08398cc0f2 |
@ -22,6 +22,7 @@
|
||||
|
||||
- Gmail: `--body-file` for `send`, `drafts create`, and `drafts update` (use `-` for stdin) to send multi-line plain text.
|
||||
- Drive: `gog drive drives` lists shared drives (Team Drives). (#67) — thanks @pasogott.
|
||||
- Sheets: `gog sheets format` applies cell formatting via `--format-json` + `--format-fields`. (#72) — thanks @nilzzzzzz.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@ -610,6 +610,7 @@ gog slides export <presentationId> --format pdf --out ./deck.pdf
|
||||
# Sheets
|
||||
gog sheets copy <spreadsheetId> "My Sheet Copy"
|
||||
gog sheets export <spreadsheetId> --format pdf --out ./sheet.pdf
|
||||
gog sheets format <spreadsheetId> 'Sheet1!A1:B2' --format-json '{"textFormat":{"bold":true}}' --format-fields 'userEnteredFormat.textFormat.bold'
|
||||
```
|
||||
|
||||
### Contacts
|
||||
@ -679,6 +680,9 @@ gog sheets append <spreadsheetId> 'Sheet1!A:C' 'new|row|data'
|
||||
gog sheets append <spreadsheetId> 'Sheet1!A:C' 'new|row|data' --copy-validation-from 'Sheet1!A2:C2'
|
||||
gog sheets clear <spreadsheetId> 'Sheet1!A1:B10'
|
||||
|
||||
# Format
|
||||
gog sheets format <spreadsheetId> 'Sheet1!A1:B2' --format-json '{"textFormat":{"bold":true}}' --format-fields 'userEnteredFormat.textFormat.bold'
|
||||
|
||||
# Create
|
||||
gog sheets create "My New Spreadsheet" --sheets "Sheet1,Sheet2"
|
||||
```
|
||||
|
||||
@ -28,6 +28,7 @@ type SheetsCmd struct {
|
||||
Update SheetsUpdateCmd `cmd:"" name:"update" help:"Update values in a range"`
|
||||
Append SheetsAppendCmd `cmd:"" name:"append" help:"Append values to a range"`
|
||||
Clear SheetsClearCmd `cmd:"" name:"clear" help:"Clear values in a range"`
|
||||
Format SheetsFormatCmd `cmd:"" name:"format" help:"Apply cell formatting to a range"`
|
||||
Metadata SheetsMetadataCmd `cmd:"" name:"metadata" help:"Get spreadsheet metadata"`
|
||||
Create SheetsCreateCmd `cmd:"" name:"create" help:"Create a new spreadsheet"`
|
||||
Copy SheetsCopyCmd `cmd:"" name:"copy" help:"Copy a Google Sheet"`
|
||||
|
||||
100
internal/cmd/sheets_format.go
Normal file
100
internal/cmd/sheets_format.go
Normal file
@ -0,0 +1,100 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/api/sheets/v4"
|
||||
|
||||
"github.com/steipete/gogcli/internal/outfmt"
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
)
|
||||
|
||||
type SheetsFormatCmd struct {
|
||||
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
|
||||
Range string `arg:"" name:"range" help:"Range (eg. Sheet1!A1:B2)"`
|
||||
FormatJSON string `name:"format-json" help:"Cell format as JSON (Sheets API CellFormat)"`
|
||||
FormatFields string `name:"format-fields" help:"Format field mask (eg. userEnteredFormat.textFormat.bold)"`
|
||||
}
|
||||
|
||||
func (c *SheetsFormatCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
u := ui.FromContext(ctx)
|
||||
account, err := requireAccount(flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spreadsheetID := strings.TrimSpace(c.SpreadsheetID)
|
||||
rangeSpec := cleanRange(c.Range)
|
||||
if spreadsheetID == "" {
|
||||
return usage("empty spreadsheetId")
|
||||
}
|
||||
if strings.TrimSpace(rangeSpec) == "" {
|
||||
return usage("empty range")
|
||||
}
|
||||
if strings.TrimSpace(c.FormatJSON) == "" {
|
||||
return fmt.Errorf("provide format JSON via --format-json")
|
||||
}
|
||||
formatFields := strings.TrimSpace(c.FormatFields)
|
||||
if formatFields == "" {
|
||||
return fmt.Errorf("provide format fields via --format-fields")
|
||||
}
|
||||
|
||||
var format sheets.CellFormat
|
||||
if err := json.Unmarshal([]byte(c.FormatJSON), &format); err != nil {
|
||||
return fmt.Errorf("invalid format JSON: %w", err)
|
||||
}
|
||||
if err := applyForceSendFields(&format, formatFields); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rangeInfo, err := parseSheetRange(rangeSpec, "format")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := newSheetsService(ctx, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sheetIDs, err := fetchSheetIDMap(ctx, svc, spreadsheetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gridRange, err := gridRangeFromMap(rangeInfo, sheetIDs, "format")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &sheets.BatchUpdateSpreadsheetRequest{
|
||||
Requests: []*sheets.Request{
|
||||
{
|
||||
RepeatCell: &sheets.RepeatCellRequest{
|
||||
Range: gridRange,
|
||||
Cell: &sheets.CellData{
|
||||
UserEnteredFormat: &format,
|
||||
},
|
||||
Fields: formatFields,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := svc.Spreadsheets.BatchUpdate(spreadsheetID, req).Do(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outfmt.IsJSON(ctx) {
|
||||
return outfmt.WriteJSON(os.Stdout, map[string]any{
|
||||
"range": rangeSpec,
|
||||
"fields": formatFields,
|
||||
})
|
||||
}
|
||||
|
||||
u.Out().Printf("Formatted %s", rangeSpec)
|
||||
return nil
|
||||
}
|
||||
145
internal/cmd/sheets_format_fields.go
Normal file
145
internal/cmd/sheets_format_fields.go
Normal file
@ -0,0 +1,145 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/api/sheets/v4"
|
||||
)
|
||||
|
||||
func applyForceSendFields(format *sheets.CellFormat, fieldMask string) error {
|
||||
if format == nil {
|
||||
return fmt.Errorf("format is required")
|
||||
}
|
||||
|
||||
for _, raw := range splitFieldMask(fieldMask) {
|
||||
normalized := normalizeFormatField(raw)
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
if err := forceSendJSONField(format, normalized); err != nil {
|
||||
return fmt.Errorf("invalid format field %q: %w", strings.TrimSpace(raw), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitFieldMask(mask string) []string {
|
||||
if strings.TrimSpace(mask) == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(mask, ",")
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func normalizeFormatField(field string) string {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
return ""
|
||||
}
|
||||
if field == "userEnteredFormat" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(field, "userEnteredFormat.") {
|
||||
return strings.TrimPrefix(field, "userEnteredFormat.")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func forceSendJSONField(root any, jsonPath string) error {
|
||||
current := reflect.ValueOf(root)
|
||||
if current.Kind() != reflect.Pointer || current.IsNil() {
|
||||
return fmt.Errorf("format must be a non-nil pointer")
|
||||
}
|
||||
|
||||
parts := strings.Split(jsonPath, ".")
|
||||
for i, part := range parts {
|
||||
if current.Kind() == reflect.Pointer {
|
||||
if current.IsNil() {
|
||||
if current.Type().Elem().Kind() != reflect.Struct {
|
||||
return fmt.Errorf("field %q is not a struct", part)
|
||||
}
|
||||
current.Set(reflect.New(current.Type().Elem()))
|
||||
}
|
||||
current = current.Elem()
|
||||
}
|
||||
if current.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("field %q is not a struct", part)
|
||||
}
|
||||
|
||||
fieldValue, fieldName, ok := findJSONField(current, part)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown field %q", part)
|
||||
}
|
||||
|
||||
if i == len(parts)-1 {
|
||||
if fieldValue.Kind() == reflect.Pointer && fieldValue.IsNil() && fieldValue.Type().Elem().Kind() == reflect.Struct {
|
||||
fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
|
||||
}
|
||||
return addForceSendField(current, fieldName)
|
||||
}
|
||||
|
||||
switch fieldValue.Kind() {
|
||||
case reflect.Pointer:
|
||||
if fieldValue.IsNil() {
|
||||
if fieldValue.Type().Elem().Kind() != reflect.Struct {
|
||||
return fmt.Errorf("field %q is not a struct", part)
|
||||
}
|
||||
fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
|
||||
}
|
||||
current = fieldValue
|
||||
case reflect.Struct:
|
||||
if !fieldValue.CanAddr() {
|
||||
return fmt.Errorf("field %q is not addressable", part)
|
||||
}
|
||||
current = fieldValue.Addr()
|
||||
default:
|
||||
return fmt.Errorf("field %q is not a struct", part)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findJSONField(v reflect.Value, jsonName string) (reflect.Value, string, bool) {
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if field.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
tag := field.Tag.Get("json")
|
||||
if tag == "-" {
|
||||
continue
|
||||
}
|
||||
name := strings.Split(tag, ",")[0]
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if name == jsonName {
|
||||
return v.Field(i), field.Name, true
|
||||
}
|
||||
}
|
||||
return reflect.Value{}, "", false
|
||||
}
|
||||
|
||||
func addForceSendField(v reflect.Value, fieldName string) error {
|
||||
fs := v.FieldByName("ForceSendFields")
|
||||
if !fs.IsValid() {
|
||||
return fmt.Errorf("missing ForceSendFields")
|
||||
}
|
||||
if fs.Kind() != reflect.Slice || fs.Type().Elem().Kind() != reflect.String {
|
||||
return fmt.Errorf("invalid ForceSendFields")
|
||||
}
|
||||
for i := 0; i < fs.Len(); i++ {
|
||||
if fs.Index(i).String() == fieldName {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
fs.Set(reflect.Append(fs, reflect.ValueOf(fieldName)))
|
||||
return nil
|
||||
}
|
||||
36
internal/cmd/sheets_format_fields_test.go
Normal file
36
internal/cmd/sheets_format_fields_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"google.golang.org/api/sheets/v4"
|
||||
)
|
||||
|
||||
func TestApplyForceSendFields_TextFormatBold(t *testing.T) {
|
||||
format := sheets.CellFormat{}
|
||||
if err := applyForceSendFields(&format, "userEnteredFormat.textFormat.bold"); err != nil {
|
||||
t.Fatalf("applyForceSendFields: %v", err)
|
||||
}
|
||||
if format.TextFormat == nil {
|
||||
t.Fatalf("expected textFormat to be allocated")
|
||||
}
|
||||
if !hasString(format.TextFormat.ForceSendFields, "Bold") {
|
||||
t.Fatalf("expected Bold to be force-sent, got %#v", format.TextFormat.ForceSendFields)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyForceSendFields_UnknownField(t *testing.T) {
|
||||
format := sheets.CellFormat{}
|
||||
if err := applyForceSendFields(&format, "userEnteredFormat.nope"); err == nil {
|
||||
t.Fatalf("expected error for unknown field")
|
||||
}
|
||||
}
|
||||
|
||||
func hasString(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
106
internal/cmd/sheets_format_test.go
Normal file
106
internal/cmd/sheets_format_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/sheets/v4"
|
||||
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
)
|
||||
|
||||
func TestSheetsFormatCmd(t *testing.T) {
|
||||
origNew := newSheetsService
|
||||
t.Cleanup(func() { newSheetsService = origNew })
|
||||
|
||||
var gotRepeat *sheets.RepeatCellRequest
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/sheets/v4")
|
||||
path = strings.TrimPrefix(path, "/v4")
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/spreadsheets/s1") && r.Method == http.MethodGet:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"spreadsheetId": "s1",
|
||||
"sheets": []map[string]any{
|
||||
{"properties": map[string]any{"sheetId": 42, "title": "Sheet1"}},
|
||||
},
|
||||
})
|
||||
return
|
||||
case strings.Contains(path, "/spreadsheets/s1:batchUpdate") && r.Method == http.MethodPost:
|
||||
var req sheets.BatchUpdateSpreadsheetRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
t.Fatalf("decode batchUpdate: %v", err)
|
||||
}
|
||||
if len(req.Requests) != 1 || req.Requests[0].RepeatCell == nil {
|
||||
t.Fatalf("expected repeatCell request, got %#v", req.Requests)
|
||||
}
|
||||
gotRepeat = req.Requests[0].RepeatCell
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{})
|
||||
return
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc, err := sheets.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
newSheetsService = func(context.Context, string) (*sheets.Service, error) { return svc, nil }
|
||||
|
||||
flags := &RootFlags{Account: "a@b.com"}
|
||||
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
|
||||
if uiErr != nil {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
cmd := &SheetsFormatCmd{}
|
||||
if err := runKong(t, cmd, []string{
|
||||
"s1",
|
||||
"Sheet1!B2:C3",
|
||||
"--format-json", `{"textFormat":{"bold":true}}`,
|
||||
"--format-fields", "userEnteredFormat.textFormat.bold",
|
||||
}, ctx, flags); err != nil {
|
||||
t.Fatalf("format: %v", err)
|
||||
}
|
||||
|
||||
if gotRepeat == nil {
|
||||
t.Fatal("expected repeatCell request")
|
||||
}
|
||||
if gotRepeat.Fields != "userEnteredFormat.textFormat.bold" {
|
||||
t.Fatalf("unexpected fields: %s", gotRepeat.Fields)
|
||||
}
|
||||
if gotRepeat.Range == nil {
|
||||
t.Fatalf("missing range")
|
||||
}
|
||||
if gotRepeat.Range.SheetId != 42 {
|
||||
t.Fatalf("unexpected sheet id: %d", gotRepeat.Range.SheetId)
|
||||
}
|
||||
if gotRepeat.Range.StartRowIndex != 1 || gotRepeat.Range.EndRowIndex != 3 {
|
||||
t.Fatalf("unexpected row range: %#v", gotRepeat.Range)
|
||||
}
|
||||
if gotRepeat.Range.StartColumnIndex != 1 || gotRepeat.Range.EndColumnIndex != 3 {
|
||||
t.Fatalf("unexpected column range: %#v", gotRepeat.Range)
|
||||
}
|
||||
if gotRepeat.Cell == nil || gotRepeat.Cell.UserEnteredFormat == nil || gotRepeat.Cell.UserEnteredFormat.TextFormat == nil {
|
||||
t.Fatalf("missing format data: %#v", gotRepeat.Cell)
|
||||
}
|
||||
if !gotRepeat.Cell.UserEnteredFormat.TextFormat.Bold {
|
||||
t.Fatalf("expected bold text format, got %#v", gotRepeat.Cell.UserEnteredFormat.TextFormat)
|
||||
}
|
||||
}
|
||||
@ -9,20 +9,13 @@ import (
|
||||
)
|
||||
|
||||
func copyDataValidation(ctx context.Context, svc *sheets.Service, spreadsheetID, sourceA1, destA1 string) error {
|
||||
sourceRange, err := parseA1Range(sourceA1)
|
||||
sourceRange, err := parseSheetRange(sourceA1, "copy-validation-from")
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse copy-validation-from: %w", err)
|
||||
return err
|
||||
}
|
||||
destRange, err := parseA1Range(destA1)
|
||||
destRange, err := parseSheetRange(destA1, "updated")
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse updated range: %w", err)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(sourceRange.SheetName) == "" {
|
||||
return fmt.Errorf("copy-validation-from must include a sheet name")
|
||||
}
|
||||
if strings.TrimSpace(destRange.SheetName) == "" {
|
||||
return fmt.Errorf("updated range missing sheet name")
|
||||
return err
|
||||
}
|
||||
|
||||
sheetIDs, err := fetchSheetIDMap(ctx, svc, spreadsheetID)
|
||||
@ -30,21 +23,21 @@ func copyDataValidation(ctx context.Context, svc *sheets.Service, spreadsheetID,
|
||||
return err
|
||||
}
|
||||
|
||||
sourceSheetID, ok := sheetIDs[sourceRange.SheetName]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown sheet %q in copy-validation-from", sourceRange.SheetName)
|
||||
sourceGrid, err := gridRangeFromMap(sourceRange, sheetIDs, "copy-validation-from")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destSheetID, ok := sheetIDs[destRange.SheetName]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown sheet %q in updated range", destRange.SheetName)
|
||||
destGrid, err := gridRangeFromMap(destRange, sheetIDs, "updated")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &sheets.BatchUpdateSpreadsheetRequest{
|
||||
Requests: []*sheets.Request{
|
||||
{
|
||||
CopyPaste: &sheets.CopyPasteRequest{
|
||||
Source: toGridRange(sourceRange, sourceSheetID),
|
||||
Destination: toGridRange(destRange, destSheetID),
|
||||
Source: sourceGrid,
|
||||
Destination: destGrid,
|
||||
PasteType: "PASTE_DATA_VALIDATION",
|
||||
},
|
||||
},
|
||||
@ -88,3 +81,22 @@ func toGridRange(r a1Range, sheetID int64) *sheets.GridRange {
|
||||
EndColumnIndex: int64(r.EndCol),
|
||||
}
|
||||
}
|
||||
|
||||
func parseSheetRange(a1, label string) (a1Range, error) {
|
||||
r, err := parseA1Range(a1)
|
||||
if err != nil {
|
||||
return a1Range{}, fmt.Errorf("parse %s range: %w", label, err)
|
||||
}
|
||||
if strings.TrimSpace(r.SheetName) == "" {
|
||||
return a1Range{}, fmt.Errorf("%s range must include a sheet name", label)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func gridRangeFromMap(r a1Range, sheetIDs map[string]int64, label string) (*sheets.GridRange, error) {
|
||||
sheetID, ok := sheetIDs[r.SheetName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown sheet %q in %s range", r.SheetName, label)
|
||||
}
|
||||
return toGridRange(r, sheetID), nil
|
||||
}
|
||||
|
||||
@ -201,3 +201,50 @@ func TestSheetsClearMetadataCreate_ValidationErrors(t *testing.T) {
|
||||
t.Fatalf("expected create missing title error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSheetsFormat_ValidationErrors(t *testing.T) {
|
||||
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
|
||||
if uiErr != nil {
|
||||
t.Fatalf("ui.New: %v", uiErr)
|
||||
}
|
||||
ctx := ui.WithUI(context.Background(), u)
|
||||
flags := &RootFlags{Account: "a@b.com"}
|
||||
|
||||
if err := (&SheetsFormatCmd{}).Run(ctx, flags); err == nil {
|
||||
t.Fatalf("expected format missing spreadsheetId error")
|
||||
}
|
||||
if err := (&SheetsFormatCmd{SpreadsheetID: "s1"}).Run(ctx, flags); err == nil {
|
||||
t.Fatalf("expected format missing range error")
|
||||
}
|
||||
if err := (&SheetsFormatCmd{SpreadsheetID: "s1", Range: "Sheet1!A1"}).Run(ctx, flags); err == nil {
|
||||
t.Fatalf("expected format missing format-json error")
|
||||
}
|
||||
if err := (&SheetsFormatCmd{SpreadsheetID: "s1", Range: "Sheet1!A1", FormatJSON: "{\"textFormat\":{\"bold\":true}}"}).Run(ctx, flags); err == nil {
|
||||
t.Fatalf("expected format missing format-fields error")
|
||||
}
|
||||
if err := (&SheetsFormatCmd{SpreadsheetID: "s1", Range: "Sheet1!A1", FormatJSON: "nope", FormatFields: "userEnteredFormat.textFormat.bold"}).Run(ctx, flags); err == nil {
|
||||
t.Fatalf("expected format invalid json error")
|
||||
}
|
||||
if err := (&SheetsFormatCmd{SpreadsheetID: "s1", Range: "A1:B2", FormatJSON: "{\"textFormat\":{\"bold\":true}}", FormatFields: "userEnteredFormat.textFormat.bold"}).Run(ctx, flags); err == nil {
|
||||
t.Fatalf("expected format missing sheet name error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSheetRangeAndGridRange(t *testing.T) {
|
||||
if _, err := parseSheetRange("A1:B2", "format"); err == nil {
|
||||
t.Fatalf("expected missing sheet name error")
|
||||
}
|
||||
|
||||
r, err := parseSheetRange("Sheet1!B2:C3", "format")
|
||||
if err != nil {
|
||||
t.Fatalf("parseSheetRange: %v", err)
|
||||
}
|
||||
|
||||
grid, err := gridRangeFromMap(r, map[string]int64{"Sheet1": 9}, "format")
|
||||
if err != nil {
|
||||
t.Fatalf("gridRangeFromMap: %v", err)
|
||||
}
|
||||
if grid.SheetId != 9 {
|
||||
t.Fatalf("unexpected sheet id: %d", grid.SheetId)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user