diff --git a/src/Api/Action/GitLab/ListGitLabIssuesAction.php b/src/Api/Action/GitLab/ListGitLabIssuesAction.php index 3b82e7fb..8d7b2032 100644 --- a/src/Api/Action/GitLab/ListGitLabIssuesAction.php +++ b/src/Api/Action/GitLab/ListGitLabIssuesAction.php @@ -4,6 +4,8 @@ namespace mglaman\DrupalOrg\Action\GitLab; +use mglaman\DrupalOrg\Enum\ProjectIssueCategory; +use mglaman\DrupalOrg\Enum\ProjectIssueType; use mglaman\DrupalOrg\GitLab\Client as GitLabClient; use mglaman\DrupalOrg\GitLab\Entity\GitLabIssue; use mglaman\DrupalOrg\Result\GitLab\GitLabIssuesResult; @@ -14,8 +16,13 @@ public function __construct(private readonly GitLabClient $gitLabClient) { } - public function __invoke(string $projectMachineName, string $state = 'opened', int $limit = 25): GitLabIssuesResult - { + public function __invoke( + string $projectMachineName, + string $state = 'opened', + int $limit = 25, + ProjectIssueType $type = ProjectIssueType::All, + ?ProjectIssueCategory $category = null, + ): GitLabIssuesResult { $params = [ 'state' => $state, 'per_page' => $limit, @@ -23,6 +30,11 @@ public function __invoke(string $projectMachineName, string $state = 'opened', i 'sort' => 'desc', ]; + $labels = array_filter([$type->gitLabLabel(), $category?->gitLabLabel()]); + if ($labels !== []) { + $params['labels'] = implode(',', $labels); + } + $data = $this->gitLabClient->getIssues('project/' . $projectMachineName, $params); $issues = array_map( static fn(\stdClass $item) => GitLabIssue::fromStdClass($item), diff --git a/src/Api/Enum/ProjectIssueCategory.php b/src/Api/Enum/ProjectIssueCategory.php index 57172f86..aeadf0c6 100644 --- a/src/Api/Enum/ProjectIssueCategory.php +++ b/src/Api/Enum/ProjectIssueCategory.php @@ -20,4 +20,9 @@ public function categoryId(): int ProjectIssueCategory::Plan => 5, }; } + + public function gitLabLabel(): string + { + return 'category::' . $this->value; + } } diff --git a/src/Api/Enum/ProjectIssueType.php b/src/Api/Enum/ProjectIssueType.php index c5b29c0c..15ea81dc 100644 --- a/src/Api/Enum/ProjectIssueType.php +++ b/src/Api/Enum/ProjectIssueType.php @@ -7,4 +7,13 @@ enum ProjectIssueType: string case All = 'all'; case Rtbc = 'rtbc'; case Review = 'review'; + + public function gitLabLabel(): ?string + { + return match ($this) { + ProjectIssueType::All => null, + ProjectIssueType::Rtbc => 'state::rtbc', + ProjectIssueType::Review => 'state::needsReview', + }; + } } diff --git a/src/Api/Mcp/ToolRegistry.php b/src/Api/Mcp/ToolRegistry.php index cdd56950..7abe867b 100644 --- a/src/Api/Mcp/ToolRegistry.php +++ b/src/Api/Mcp/ToolRegistry.php @@ -222,8 +222,13 @@ public function gitlabProjectIssues( #[Schema(description: 'Issue state filter.', enum: ['opened', 'closed', 'all'])] string $state = 'opened', #[Schema(description: 'Maximum number of issues to return.', minimum: 1, maximum: 100)] - int $limit = 25 + int $limit = 25, + #[Schema(description: 'Filter issues by status type.', enum: ['all', 'rtbc', 'review'])] + string $type = 'all', + #[Schema(description: 'Filter issues by category.', enum: ['bug', 'task', 'feature', 'support', 'plan'])] + ?string $category = null ): mixed { - return (new ListGitLabIssuesAction(new GitLabClient()))($machineName, $state, $limit)->jsonSerialize(); + $issueCategory = $category !== null ? ProjectIssueCategory::from($category) : null; + return (new ListGitLabIssuesAction(new GitLabClient()))($machineName, $state, $limit, ProjectIssueType::from($type), $issueCategory)->jsonSerialize(); } } diff --git a/src/Cli/Command/Project/ProjectIssues.php b/src/Cli/Command/Project/ProjectIssues.php index ccaca0f0..11ee64fa 100644 --- a/src/Cli/Command/Project/ProjectIssues.php +++ b/src/Cli/Command/Project/ProjectIssues.php @@ -70,10 +70,18 @@ protected function execute( ): int { $format = (string) $this->stdIn->getOption('format'); $limit = (int) $this->stdIn->getOption('limit'); + $type = ProjectIssueType::from((string) $this->stdIn->getArgument('type')); + $categoryOption = $this->stdIn->getOption('category'); + $category = $categoryOption !== null ? ProjectIssueCategory::from((string) $categoryOption) : null; if (!$this->projectData->hasIssueQueue) { $this->stdErr->writeln('Project uses GitLab work items (no Drupal.org issue queue).'); - $result = (new ListGitLabIssuesAction(new GitLabClient()))($this->projectData->machineName, limit: $limit); + $result = (new ListGitLabIssuesAction(new GitLabClient()))( + $this->projectData->machineName, + limit: $limit, + type: $type, + category: $category, + ); if ($this->writeFormatted($result, $format)) { return 0; } @@ -98,11 +106,9 @@ protected function execute( } $action = new GetProjectIssuesAction($this->client); - $categoryOption = $this->stdIn->getOption('category'); - $category = $categoryOption !== null ? ProjectIssueCategory::from((string) $categoryOption) : null; $result = $action( $this->projectData, - ProjectIssueType::from((string) $this->stdIn->getArgument('type')), + $type, (string) $this->stdIn->getOption('core'), $limit, $category diff --git a/tests/src/Action/GitLab/ListGitLabIssuesActionTest.php b/tests/src/Action/GitLab/ListGitLabIssuesActionTest.php new file mode 100644 index 00000000..8b84e8dc --- /dev/null +++ b/tests/src/Action/GitLab/ListGitLabIssuesActionTest.php @@ -0,0 +1,66 @@ + + */ + public static function filterProvider(): iterable + { + yield 'no filters' => [ProjectIssueType::All, null, null]; + yield 'rtbc' => [ProjectIssueType::Rtbc, null, 'state::rtbc']; + yield 'review' => [ProjectIssueType::Review, null, 'state::needsReview']; + yield 'category only' => [ProjectIssueType::All, ProjectIssueCategory::Bug, 'category::bug']; + yield 'type and category' => [ProjectIssueType::Rtbc, ProjectIssueCategory::Task, 'state::rtbc,category::task']; + } + + #[DataProvider('filterProvider')] + public function testFiltersMapToGitLabLabels( + ProjectIssueType $type, + ?ProjectIssueCategory $category, + ?string $expectedLabels, + ): void { + $expectedParams = [ + 'state' => 'opened', + 'per_page' => 10, + 'order_by' => 'created_at', + 'sort' => 'desc', + ]; + if ($expectedLabels !== null) { + $expectedParams['labels'] = $expectedLabels; + } + + $gitLabClient = $this->createMock(GitLabClient::class); + $gitLabClient->expects(self::once()) + ->method('getIssues') + ->with('project/smart_content', $expectedParams) + ->willReturn([]); + + $result = (new ListGitLabIssuesAction($gitLabClient))( + 'smart_content', + limit: 10, + type: $type, + category: $category, + ); + + self::assertSame('smart_content', $result->projectMachineName); + self::assertSame([], $result->issues); + } +}