Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/Services/BatchOperations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Services;

use ConduitUi\GitHubConnector\Connector;
use ConduitUI\Issue\Support\IssueCollection;
use Exception;
use Illuminate\Support\Collection;

class BatchOperations
{
public function __construct(
protected readonly Connector $connector,
protected readonly string $owner,
protected readonly string $repo,
) {}

/**
* Perform batch operation with progress tracking.
*
* @param callable(mixed): mixed $operation
* @param callable(int, int, mixed): void|null $progress
* @return Collection<int, array<string, mixed>>
*/
public function batch(IssueCollection $issues, callable $operation, ?callable $progress = null): Collection
{
$results = new Collection;
$total = $issues->count();
$current = 0;

foreach ($issues as $issue) {
$current++;

try {
$result = $operation($issue);
$results->push([
'issue' => $issue->number,
'success' => true,
'result' => $result,
]);
} catch (Exception $e) {
$results->push([
'issue' => $issue->number,
'success' => false,
'error' => $e->getMessage(),
]);
}

if ($progress !== null) {
$progress($current, $total, $issue);
}
}

return $results;
}

/**
* Get a new IssueQuery instance.
*/
protected function query(): IssueQuery
{
return new IssueQuery($this->connector, $this->owner, $this->repo);
}
}
8 changes: 3 additions & 5 deletions src/Services/IssueQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
use ConduitUi\GitHubConnector\Connector;
use ConduitUI\Issue\Data\Issue;
use ConduitUI\Issue\Requests\Issues\ListIssuesRequest;
use ConduitUI\Issue\Support\IssueCollection;
use DateTime;
use Illuminate\Support\Collection;

class IssueQuery
{
Expand Down Expand Up @@ -190,10 +190,8 @@ public function page(int $page): self

/**
* Execute the query and get all issues.
*
* @return \Illuminate\Support\Collection<int, \ConduitUI\Issue\Data\Issue>
*/
public function get(): Collection
public function get(): IssueCollection
{
// Remove client-side filters before sending request
$apiFilters = $this->filters;
Expand Down Expand Up @@ -222,7 +220,7 @@ public function get(): Collection
$collection = $collection->filter(fn (Issue $issue): bool => $issue->updatedAt <= $updatedBefore);
}

return $collection;
return new IssueCollection($collection);
}

/**
Expand Down
183 changes: 183 additions & 0 deletions src/Support/IssueCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Support;

use ConduitUI\Issue\Data\Issue;
use ConduitUI\Issue\Data\Label;
use Illuminate\Support\Collection;

/**
* @extends Collection<int, Issue>
*/
class IssueCollection extends Collection
{
/**
* Filter issues by label.
*/
public function withLabel(string $label): self
{
return $this->filter(function (Issue $issue) use ($label): bool {
foreach ($issue->labels as $issueLabel) {
if ($issueLabel->name === $label) {
return true;
}
}

return false;
});
}

/**
* Filter issues without label.
*/
public function withoutLabel(string $label): self
{
return $this->reject(function (Issue $issue) use ($label): bool {
foreach ($issue->labels as $issueLabel) {
if ($issueLabel->name === $label) {
return true;
}
}

return false;
});
}

/**
* Filter open issues.
*/
public function open(): self
{
return $this->filter(fn (Issue $issue): bool => $issue->isOpen());
}

/**
* Filter closed issues.
*/
public function closed(): self
{
return $this->filter(fn (Issue $issue): bool => $issue->isClosed());
}

/**
* Filter assigned issues.
*/
public function assigned(): self
{
return $this->filter(fn (Issue $issue): bool => count($issue->assignees) > 0);
}

/**
* Filter unassigned issues.
*/
public function unassigned(): self
{
return $this->filter(fn (Issue $issue): bool => count($issue->assignees) === 0);
}

/**
* Group by label.
*
* @return Collection<string, IssueCollection>
*/
public function groupByLabel(): Collection
{
$grouped = new Collection;

foreach ($this as $issue) {
foreach ($issue->labels as $label) {
if (! $grouped->has($label->name)) {
$grouped->put($label->name, new static);
}
$grouped->get($label->name)->push($issue);
}
}

return $grouped;
}

/**
* Group by state.
*
* @return Collection<string, IssueCollection>
*/
public function groupByState(): Collection
{
return $this->groupBy('state')->map(fn ($items) => new static($items));
}

/**
* Group by assignee.
*
* @return Collection<string, IssueCollection>
*/
public function groupByAssignee(): Collection
{
$grouped = new Collection;

foreach ($this as $issue) {
if (count($issue->assignees) === 0) {
if (! $grouped->has('unassigned')) {
$grouped->put('unassigned', new static);
}
$grouped->get('unassigned')->push($issue);
} else {
foreach ($issue->assignees as $assignee) {
if (! $grouped->has($assignee->login)) {
$grouped->put($assignee->login, new static);
}
$grouped->get($assignee->login)->push($issue);
}
}
}

return $grouped;
}

/**
* Get statistics.
*
* @return array<string, mixed>
*/
public function statistics(): array
{
$labels = collect();
$assignees = collect();

foreach ($this as $issue) {
foreach ($issue->labels as $label) {
$labels->push($label->name);
}

foreach ($issue->assignees as $assignee) {
$assignees->push($assignee->login);
}
}

return [
'total' => $this->count(),
'open' => $this->open()->count(),
'closed' => $this->closed()->count(),
'assigned' => $this->assigned()->count(),
'unassigned' => $this->unassigned()->count(),
'labels' => $labels->unique()->sort()->values()->all(),
'assignees' => $assignees->unique()->sort()->values()->all(),
];
}

/**
* Check if an issue has a specific label.
*/
protected function hasLabel(Issue $issue, string $label): bool
{
foreach ($issue->labels as $issueLabel) {
if ($issueLabel->name === $label) {
return true;
}
}

return false;
}
}
Loading