diff --git a/api/search.go b/api/search.go index b30513a..af31661 100644 --- a/api/search.go +++ b/api/search.go @@ -45,6 +45,16 @@ type SearchContainer struct { DisplayURL string `json:"displayUrl"` } +// SpaceKey extracts the space key from the DisplayURL. +// DisplayURL is typically in the format "/display/SPACEKEY" or "/spaces/SPACEKEY/...". +func (c SearchContainer) SpaceKey() string { + parts := strings.Split(strings.TrimPrefix(c.DisplayURL, "/"), "/") + if len(parts) >= 2 && (parts[0] == "display" || parts[0] == "spaces") { + return parts[1] + } + return "" +} + // SearchResponse represents the v1 search API response. type SearchResponse struct { Results []SearchResult `json:"results"` diff --git a/api/search_test.go b/api/search_test.go index 650c924..9fdbc70 100644 --- a/api/search_test.go +++ b/api/search_test.go @@ -261,6 +261,27 @@ func TestBuildCQL_Empty(t *testing.T) { assert.Empty(t, cql) } +func TestSearchContainer_SpaceKey(t *testing.T) { + tests := []struct { + name string + displayURL string + expected string + }{ + {"display format", "/display/DEV", "DEV"}, + {"spaces format", "/spaces/TEAM/overview", "TEAM"}, + {"empty", "", ""}, + {"no matching prefix", "/other/path", ""}, + {"only prefix", "/display/", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := SearchContainer{DisplayURL: tt.displayURL} + assert.Equal(t, tt.expected, c.SpaceKey()) + }) + } +} + func TestBuildCQL_QuotesInValue(t *testing.T) { opts := &SearchOptions{Text: `search "quoted" term`} cql := buildCQL(opts) diff --git a/internal/cmd/page/view.go b/internal/cmd/page/view.go index c6a971b..ffc6764 100644 --- a/internal/cmd/page/view.go +++ b/internal/cmd/page/view.go @@ -112,6 +112,18 @@ func runView(pageID string, opts *viewOptions, client *api.Client) error { renderer := view.NewRenderer(view.Format(opts.output), opts.noColor) if opts.output == "json" { + // Enrich JSON output with spaceKey if we can resolve it + if page.SpaceID != "" { + space, err := client.GetSpace(context.Background(), page.SpaceID) + if err == nil { + // Create enriched response with spaceKey + type enrichedPage struct { + *api.Page + SpaceKey string `json:"spaceKey"` + } + return renderer.RenderJSON(enrichedPage{Page: page, SpaceKey: space.Key}) + } + } return renderer.RenderJSON(page) } @@ -119,6 +131,14 @@ func runView(pageID string, opts *viewOptions, client *api.Client) error { if !opts.contentOnly { renderer.RenderKeyValue("Title", page.Title) renderer.RenderKeyValue("ID", page.ID) + if page.SpaceID != "" { + space, err := client.GetSpace(context.Background(), page.SpaceID) + if err == nil { + renderer.RenderKeyValue("Space", fmt.Sprintf("%s (ID: %s)", space.Key, page.SpaceID)) + } else { + renderer.RenderKeyValue("Space ID", page.SpaceID) + } + } if page.Version != nil { renderer.RenderKeyValue("Version", fmt.Sprintf("%d", page.Version.Number)) } diff --git a/internal/cmd/page/view_test.go b/internal/cmd/page/view_test.go index a496267..0cd0bbb 100644 --- a/internal/cmd/page/view_test.go +++ b/internal/cmd/page/view_test.go @@ -14,13 +14,20 @@ import ( func TestRunView_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Contains(t, r.URL.Path, "/pages/12345") assert.Equal(t, "GET", r.Method) + if strings.Contains(r.URL.Path, "/spaces/") { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"id": "9999", "key": "DEV", "name": "Development"}`)) + return + } + + assert.Contains(t, r.URL.Path, "/pages/12345") w.WriteHeader(http.StatusOK) w.Write([]byte(`{ "id": "12345", "title": "Test Page", + "spaceId": "9999", "version": {"number": 3}, "body": {"storage": {"value": "

Hello World

"}}, "_links": {"webui": "/pages/12345"} @@ -62,10 +69,17 @@ func TestRunView_RawFormat(t *testing.T) { func TestRunView_JSONOutput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "/spaces/") { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"id": "9999", "key": "DEV", "name": "Development"}`)) + return + } + w.WriteHeader(http.StatusOK) w.Write([]byte(`{ "id": "12345", "title": "Test Page", + "spaceId": "9999", "version": {"number": 1}, "body": {"storage": {"value": "

Content

"}}, "_links": {"webui": "/pages/12345"} @@ -278,6 +292,3 @@ func TestRunView_ContentOnly_EmptyBody(t *testing.T) { require.NoError(t, err) // Output should be "(No content)" without metadata headers } - -// Ensure strings is used -var _ = strings.NewReader("") diff --git a/internal/cmd/search/search.go b/internal/cmd/search/search.go index e3f0b57..eada397 100644 --- a/internal/cmd/search/search.go +++ b/internal/cmd/search/search.go @@ -171,14 +171,16 @@ func runSearch(opts *searchOptions, client *api.Client) error { } // Render results - headers := []string{"ID", "TYPE", "SPACE", "TITLE"} + headers := []string{"ID", "TYPE", "SPACE KEY", "SPACE", "TITLE"} var rows [][]string for _, r := range result.Results { space := r.ResultGlobalContainer.Title + spaceKey := r.ResultGlobalContainer.SpaceKey() rows = append(rows, []string{ r.Content.ID, r.Content.Type, + spaceKey, view.Truncate(space, 15), view.Truncate(r.Content.Title, 50), })