Skip to content

Commit 57e3283

Browse files
authored
fix(docs): target page layout by tab (#878)
Co-authored-by: Jonathan Greene <[email protected]>
1 parent 7af5c68 commit 57e3283

7 files changed

Lines changed: 134 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 0.30.1 - Unreleased
44

5+
- Docs: honor `--tab` when setting document layout so `page-layout --tab` (and `write --pageless --tab`) target the specified tab instead of always the default tab. Page layout is per-tab; previously these silently no-opped on secondary tabs of multi-tab documents. (#878) — thanks @atmasphere.
56
- Auth: recover from corrupt stored OAuth token payloads by routing only classified decode corruption through the normal re-authentication flow while preserving operational keyring errors. (#872, #874) — thanks @KrasimirKralev.
67
- Calendar: add repeatable or comma-separated `events --event-types` filtering across single, selected, and all-calendar listings while preserving the existing unfiltered default. (#870) — thanks @malob.
78
- Gmail: render quoted-reply and forwarded-message dates in Gmail's human-readable style using the configured account timezone. (#873) — thanks @malob.

docs/commands/gog-docs-page-layout.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ gog docs (doc) page-layout (set-page-layout,page-setup) <docId> [flags]
4444
| `--readonly` | `bool` | false | Block mutating API requests at runtime; auth add also requests read-only OAuth scopes |
4545
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
4646
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
47+
| `--tab` | `string` | | Target a specific tab by title or ID (see docs list-tabs). Page layout is per-tab; omit for the default tab. |
4748
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
4849
| `--version` | `kong.VersionFlag` | | Print version and exit |
4950
| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers |

internal/cmd/docs_edit.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,21 @@ func (c *DocsWriteCmd) applyDocumentStyle(ctx context.Context, svc *docs.Service
204204
if c.Pageless {
205205
mode = docsDocumentModePageless
206206
}
207+
// Document-style fields are per-tab. Resolve --tab to a concrete tab ID so
208+
// pageless/layout lands on the targeted tab rather than silently hitting the
209+
// default tab. resolveDocsTabID is a no-op when the tab is already a concrete
210+
// ID (as in the plain-text path) and skipped entirely when no tab was given.
211+
tabID := ""
212+
if tab := strings.TrimSpace(c.Tab); tab != "" {
213+
resolved, err := resolveDocsTabID(ctx, svc, docID, tab)
214+
if err != nil {
215+
return fmt.Errorf("resolve tab %q: %w", tab, err)
216+
}
217+
tabID = resolved
218+
}
207219
if err := setDocumentStyle(ctx, svc, docID, docsDocumentStyleOptions{
208220
Mode: mode,
221+
TabID: tabID,
209222
DocsLayoutFlags: c.Layout,
210223
}); err != nil {
211224
return fmt.Errorf("set document style: %w", err)

internal/cmd/docs_layout.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ func (f DocsLayoutFlags) dryRunPayload() map[string]any {
5858

5959
type docsDocumentStyleOptions struct {
6060
Mode string
61+
// TabID, when set, targets a specific tab. Document-style fields (e.g.
62+
// documentMode/pageless) are per-tab; an empty TabID applies to the
63+
// document's default tab.
64+
TabID string
6165
DocsLayoutFlags
6266
}
6367

@@ -141,6 +145,7 @@ func buildUpdateDocumentStyleRequest(opts docsDocumentStyleOptions) (*docs.Updat
141145
return &docs.UpdateDocumentStyleRequest{
142146
DocumentStyle: style,
143147
Fields: strings.Join(fields, ","),
148+
TabId: strings.TrimSpace(opts.TabID),
144149
}, nil
145150
}
146151

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
// buildUpdateDocumentStyleRequest must carry the TabId through to the Docs API
6+
// so document-style changes (e.g. pageless) can target a non-default tab.
7+
// Regression guard for: page-layout/write --pageless silently hitting only the
8+
// default tab in multi-tab docs.
9+
func TestBuildUpdateDocumentStyleRequest_TabID(t *testing.T) {
10+
t.Parallel()
11+
12+
req, err := buildUpdateDocumentStyleRequest(docsDocumentStyleOptions{
13+
Mode: docsDocumentModePageless,
14+
TabID: "t.abc123",
15+
})
16+
if err != nil {
17+
t.Fatalf("build: %v", err)
18+
}
19+
if req.TabId != "t.abc123" {
20+
t.Fatalf("expected TabId t.abc123, got %q", req.TabId)
21+
}
22+
if req.DocumentStyle == nil || req.DocumentStyle.DocumentFormat == nil ||
23+
req.DocumentStyle.DocumentFormat.DocumentMode != docsDocumentModePageless {
24+
t.Fatalf("expected pageless documentMode, got %#v", req.DocumentStyle)
25+
}
26+
}
27+
28+
// Default behaviour (no tab) must leave TabId empty so the request applies to
29+
// the document's default tab, preserving existing single-tab behaviour.
30+
func TestBuildUpdateDocumentStyleRequest_NoTabID(t *testing.T) {
31+
t.Parallel()
32+
33+
req, err := buildUpdateDocumentStyleRequest(docsDocumentStyleOptions{
34+
Mode: docsDocumentModePageless,
35+
})
36+
if err != nil {
37+
t.Fatalf("build: %v", err)
38+
}
39+
if req.TabId != "" {
40+
t.Fatalf("expected empty TabId, got %q", req.TabId)
41+
}
42+
}

internal/cmd/docs_set_page_layout.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type DocsPageLayoutCmd struct {
2222
DocID string `arg:"" name:"docId" help:"Doc ID"`
2323
Layout string `name:"layout" enum:"pageless,pages,paged" default:"pageless" help:"Page layout: pageless or pages"`
2424
LayoutFlags DocsLayoutFlags `embed:""`
25+
Tab string `name:"tab" help:"Target a specific tab by title or ID (see docs list-tabs). Page layout is per-tab; omit for the default tab."`
26+
TabID string `name:"tab-id" hidden:"" help:"(deprecated) Use --tab"`
2527
}
2628

2729
func (c *DocsPageLayoutCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootFlags) error {
@@ -40,13 +42,21 @@ func (c *DocsPageLayoutCmd) Run(ctx context.Context, kctx *kong.Context, flags *
4042
}
4143
}
4244

45+
tab, err := resolveTabArg(ctx, c.Tab, c.TabID)
46+
if err != nil {
47+
return err
48+
}
49+
4350
dryRunPayload := map[string]any{
4451
"documentId": docID,
4552
}
4653
if mode != "" {
4754
dryRunPayload["layout"] = c.Layout
4855
dryRunPayload["mode"] = mode
4956
}
57+
if tab != "" {
58+
dryRunPayload["tab"] = tab
59+
}
5060
for k, v := range c.LayoutFlags.dryRunPayload() {
5161
dryRunPayload[k] = v
5262
}
@@ -59,8 +69,17 @@ func (c *DocsPageLayoutCmd) Run(ctx context.Context, kctx *kong.Context, flags *
5969
return err
6070
}
6171

72+
tabID := ""
73+
if tab != "" {
74+
tabID, err = resolveDocsTabID(ctx, svc, docID, tab)
75+
if err != nil {
76+
return fmt.Errorf("resolve tab %q: %w", tab, err)
77+
}
78+
}
79+
6280
if err := setDocumentStyle(ctx, svc, docID, docsDocumentStyleOptions{
6381
Mode: mode,
82+
TabID: tabID,
6483
DocsLayoutFlags: c.LayoutFlags,
6584
}); err != nil {
6685
if isDocsNotFound(err) {
@@ -77,6 +96,9 @@ func (c *DocsPageLayoutCmd) Run(ctx context.Context, kctx *kong.Context, flags *
7796
payload["layout"] = c.Layout
7897
payload["mode"] = mode
7998
}
99+
if tabID != "" {
100+
payload["tabId"] = tabID
101+
}
80102
for k, v := range c.LayoutFlags.dryRunPayload() {
81103
payload[k] = v
82104
}
@@ -87,6 +109,9 @@ func (c *DocsPageLayoutCmd) Run(ctx context.Context, kctx *kong.Context, flags *
87109
if mode != "" {
88110
u.Out().Linef("layout\t%s", c.Layout)
89111
}
112+
if tabID != "" {
113+
u.Out().Linef("tabId\t%s", tabID)
114+
}
90115
for k, v := range c.LayoutFlags.dryRunPayload() {
91116
u.Out().Linef("%s\t%s", k, v)
92117
}

internal/cmd/docs_set_page_layout_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,53 @@ func TestDocsPageLayoutCmd_Pages(t *testing.T) {
105105
}
106106
}
107107

108+
func TestDocsPageLayoutCmd_TabTitleTargetsResolvedTab(t *testing.T) {
109+
t.Parallel()
110+
111+
var update *docs.UpdateDocumentStyleRequest
112+
docSvc, cleanup := newDocsServiceForTest(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
113+
switch {
114+
case r.Method == http.MethodGet && r.URL.Path == "/v1/documents/doc1":
115+
w.Header().Set("Content-Type", "application/json")
116+
_ = json.NewEncoder(w).Encode(map[string]any{
117+
"documentId": "doc1",
118+
"tabs": []map[string]any{{
119+
"tabProperties": map[string]any{"tabId": "t.secondary", "title": "Secondary"},
120+
"documentTab": map[string]any{
121+
"body": map[string]any{"content": []map[string]any{{"startIndex": 0, "endIndex": 2}}},
122+
},
123+
}},
124+
})
125+
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, ":batchUpdate"):
126+
var req docs.BatchUpdateDocumentRequest
127+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
128+
t.Fatalf("decode: %v", err)
129+
}
130+
if len(req.Requests) != 1 {
131+
t.Fatalf("requests = %d, want 1", len(req.Requests))
132+
}
133+
update = req.Requests[0].UpdateDocumentStyle
134+
w.Header().Set("Content-Type", "application/json")
135+
_ = json.NewEncoder(w).Encode(map[string]any{"documentId": "doc1"})
136+
default:
137+
http.NotFound(w, r)
138+
}
139+
}))
140+
defer cleanup()
141+
142+
flags := &RootFlags{Account: "[email protected]"}
143+
ctx := withDocsTestService(newCmdRuntimeOutputContext(t, io.Discard, io.Discard), docSvc)
144+
if err := runKong(t, &DocsPageLayoutCmd{}, []string{"doc1", "--tab", "Secondary"}, ctx, flags); err != nil {
145+
t.Fatalf("page-layout tab: %v", err)
146+
}
147+
if update == nil {
148+
t.Fatal("missing UpdateDocumentStyle request")
149+
}
150+
if update.TabId != "t.secondary" {
151+
t.Fatalf("tabId = %q, want t.secondary", update.TabId)
152+
}
153+
}
154+
108155
func TestDocsPageLayoutCmd_EmptyDocID(t *testing.T) {
109156
t.Parallel()
110157

0 commit comments

Comments
 (0)