From 7e7d2847fefd9eda60ff18918d816fef4b3a28b6 Mon Sep 17 00:00:00 2001 From: Josep Piguillem Date: Thu, 8 Oct 2026 19:13:56 +0200 Subject: [PATCH] Point the public site to each project's TaskStream board A project with a board now carries its key, its address and the address of the board's public showcase in the public API, so the website can show its roadmap, releases and tickets for newcomers. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +++++ .../PublicApi/PublicProjectResource.php | 20 ++++++++++++++ app/Services/TaskStream/TaskStreamClient.php | 17 ++++++++++++ public/api-types.d.ts | 4 +++ public/openapi.yaml | 9 +++++++ tests/Feature/Api/PublicApiTest.php | 27 +++++++++++++++++++ 6 files changed, 83 insertions(+) diff --git a/README.md b/README.md index 5b7d83c..cb2d382 100644 --- a/README.md +++ b/README.md @@ -567,6 +567,12 @@ El cliente está en [`TaskStreamClient`](app/Services/TaskStream/TaskStreamClien y la lógica (clave, token, errores) en [`OpenInTaskStream`](app/Actions/Projects/OpenInTaskStream.php). + +En la API pública, un proyecto con tablero trae `taskstream` (`key`, `url`, +`web_url` y `showcase_url`). `showcase_url` es público en TaskStream y devuelve +su hoja de ruta, sus versiones, el sprint en curso y los tickets etiquetados +`good-first-issue`, para que la web los enseñe en la página del proyecto. + ## API pública (para el frontend) Sin autenticación y de solo lectura. Todo lo que devuelve ya está publicado en diff --git a/app/Http/Resources/PublicApi/PublicProjectResource.php b/app/Http/Resources/PublicApi/PublicProjectResource.php index dab31ff..8fd4f80 100644 --- a/app/Http/Resources/PublicApi/PublicProjectResource.php +++ b/app/Http/Resources/PublicApi/PublicProjectResource.php @@ -6,6 +6,7 @@ namespace App\Http\Resources\PublicApi; use App\Models\Media; use App\Models\Project; +use App\Services\TaskStream\TaskStreamClient; use App\Support\ImagePayload; use App\Support\SiteUrls; use App\Support\TranslatablePayload; @@ -110,11 +111,30 @@ class PublicProjectResource extends JsonResource // alive without anyone maintaining it. 'repositories' => PublicRepositoryResource::collection($this->whenLoaded('repositories')), + // Its board in TaskStream, when it has one: the public site reads + // the roadmap, releases and tickets for newcomers from there. + 'taskstream' => $this->taskstream_key === null ? null : $this->taskstream(), + 'alternates' => SiteUrls::alternates($this->resource, 'project'), 'path' => SiteUrls::alternates($this->resource, 'project')[$locale] ?? null, ]; } + /** + * @return array{key: string, url: string, web_url: string, showcase_url: string} + */ + protected function taskstream(): array + { + $client = app(TaskStreamClient::class); + + return [ + 'key' => $this->taskstream_key, + 'url' => $client->projectUrl($this->taskstream_key), + 'web_url' => $client->webUrl(), + 'showcase_url' => $client->showcaseUrl($this->taskstream_key), + ]; + } + /** * The media row when the caller loaded it, and null otherwise — never a * lazy load, because this runs once per row in a listing. diff --git a/app/Services/TaskStream/TaskStreamClient.php b/app/Services/TaskStream/TaskStreamClient.php index 817b671..e317e3e 100644 --- a/app/Services/TaskStream/TaskStreamClient.php +++ b/app/Services/TaskStream/TaskStreamClient.php @@ -85,4 +85,21 @@ class TaskStreamClient { return "{$this->webUrl}/p/{$key}"; } + + /** + * The public summary of a board (roadmap, releases, sprint, tickets for + * newcomers). Anyone may read it, so the public site can ask directly. + */ + public function showcaseUrl(string $key): string + { + return "{$this->apiUrl}/api/explore/projects/{$key}/showcase"; + } + + /** + * Where the paths of a showcase (`/p/KEY/issues/KEY-3`) are opened. + */ + public function webUrl(): string + { + return $this->webUrl; + } } diff --git a/public/api-types.d.ts b/public/api-types.d.ts index 749e830..cdaed06 100644 --- a/public/api-types.d.ts +++ b/public/api-types.d.ts @@ -241,6 +241,10 @@ export interface PublicProject { lead: PublicMember; calls: PublicCall[]; repositories: PublicRepository[]; + /** + * Its board in TaskStream, if it has one. `showcase_url` answers without a token: roadmap, releases, the sprint under way and tickets labelled good-first-issue. + */ + taskstream?: { key: string; url: string; web_url: string; showcase_url: string }; alternates: Alternates; path?: string; } diff --git a/public/openapi.yaml b/public/openapi.yaml index 45a43c4..375da4e 100644 --- a/public/openapi.yaml +++ b/public/openapi.yaml @@ -5041,6 +5041,15 @@ components: repositories: type: array items: { $ref: "#/components/schemas/PublicRepository" } + taskstream: + type: object + nullable: true + description: "Its board in TaskStream, if it has one. `showcase_url` answers without a token: roadmap, releases, the sprint under way and tickets labelled good-first-issue." + properties: + key: { type: string, example: WEB } + url: { type: string, format: uri } + web_url: { type: string, format: uri } + showcase_url: { type: string, format: uri } alternates: { $ref: "#/components/schemas/Alternates" } path: { type: string, nullable: true, example: /proyectos/mi-web } diff --git a/tests/Feature/Api/PublicApiTest.php b/tests/Feature/Api/PublicApiTest.php index f09d8be..3cb1271 100644 --- a/tests/Feature/Api/PublicApiTest.php +++ b/tests/Feature/Api/PublicApiTest.php @@ -9,6 +9,7 @@ use App\Models\Member; use App\Models\Project; use App\Models\Repository; use App\Models\Technology; +use App\Services\TaskStream\TaskStreamClient; use Illuminate\Foundation\Testing\RefreshDatabase; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -63,6 +64,32 @@ class PublicApiTest extends TestCase $this->getJson('/api/public/v1/projects/el-meu-projecte')->assertNotFound(); } + #[Test] + public function it_points_to_the_board_in_taskstream_when_there_is_one(): void + { + config([ + 'services.taskstream.api_url' => 'https://api.taskstream.test', + 'services.taskstream.web_url' => 'https://taskstream.test', + ]); + $this->app->forgetInstance(TaskStreamClient::class); + Project::factory()->published()->create([ + 'slug' => ['es' => 'con-tablero'], + 'taskstream_key' => 'WEB', + ]); + Project::factory()->published()->create(['slug' => ['es' => 'sin-tablero']]); + + $this->getJson('/api/public/v1/projects/con-tablero') + ->assertOk() + ->assertJsonPath('data.taskstream', [ + 'key' => 'WEB', + 'url' => 'https://taskstream.test/p/WEB', + 'web_url' => 'https://taskstream.test', + 'showcase_url' => 'https://api.taskstream.test/api/explore/projects/WEB/showcase', + ]); + $this->getJson('/api/public/v1/projects/sin-tablero') + ->assertJsonPath('data.taskstream', null); + } + #[Test] public function it_returns_hreflang_alternates_for_every_translated_locale(): void {