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 {