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
27 changes: 24 additions & 3 deletions src/Data/Issue.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public function __construct(
public DateTime $updatedAt,
public ?DateTime $closedAt,
public string $htmlUrl,
public string $apiUrl,
public ?string $activeLockReason,
public User $user,
public ?User $assignee = null,
public ?User $closedBy = null,
Expand All @@ -34,7 +36,7 @@ public static function fromArray(array $data): self
id: $data['id'],
number: $data['number'],
title: $data['title'],
body: $data['body'],
body: $data['body'] ?? null,
state: $data['state'],
locked: $data['locked'],
assignees: array_map(fn ($assignee) => User::fromArray($assignee), $data['assignees'] ?? []),
Expand All @@ -45,9 +47,11 @@ public static function fromArray(array $data): self
updatedAt: new DateTime($data['updated_at']),
closedAt: $data['closed_at'] ? new DateTime($data['closed_at']) : null,
htmlUrl: $data['html_url'],
apiUrl: $data['url'] ?? $data['api_url'] ?? '',
activeLockReason: $data['active_lock_reason'] ?? null,
user: User::fromArray($data['user']),
assignee: $data['assignee'] ? User::fromArray($data['assignee']) : null,
closedBy: $data['closed_by'] ? User::fromArray($data['closed_by']) : null,
assignee: isset($data['assignee']) && $data['assignee'] ? User::fromArray($data['assignee']) : null,
closedBy: isset($data['closed_by']) && $data['closed_by'] ? User::fromArray($data['closed_by']) : null,
);
}

Expand All @@ -68,6 +72,8 @@ public function toArray(): array
'updated_at' => $this->updatedAt->format('c'),
'closed_at' => $this->closedAt?->format('c'),
'html_url' => $this->htmlUrl,
'url' => $this->apiUrl,
'active_lock_reason' => $this->activeLockReason,
'user' => $this->user->toArray(),
'assignee' => $this->assignee?->toArray(),
'closed_by' => $this->closedBy?->toArray(),
Expand All @@ -83,4 +89,19 @@ public function isClosed(): bool
{
return $this->state === 'closed';
}

public function isLocked(): bool
{
return $this->locked;
}

public function hasLabel(string $label): bool
{
return in_array($label, array_map(fn (Label $l) => $l->name, $this->labels), true);
}

public function isAssignedTo(string $username): bool
{
return in_array($username, array_map(fn (User $u) => $u->login, $this->assignees), true);
}
}
1 change: 1 addition & 0 deletions src/Facades/GithubIssues.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Support\Facades\Facade;

/**
* @method static \ConduitUI\Issue\Services\IssueInstance find(string $fullName, int $number)
* @method static \Illuminate\Support\Collection listIssues(string $owner, string $repo, array $filters = [])
* @method static \ConduitUI\GithubIssues\Data\Issue getIssue(string $owner, string $repo, int $issueNumber)
* @method static \ConduitUI\GithubIssues\Data\Issue createIssue(string $owner, string $repo, array $data)
Expand Down
34 changes: 34 additions & 0 deletions src/Requests/Issues/LockIssueRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Requests\Issues;

use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;

class LockIssueRequest extends Request implements HasBody
{
use HasJsonBody;

protected Method $method = Method::PUT;

public function __construct(
protected readonly string $owner,
protected readonly string $repo,
protected readonly int $issueNumber,
protected readonly ?string $lockReason = null
) {}

public function resolveEndpoint(): string
{
return "/repos/{$this->owner}/{$this->repo}/issues/{$this->issueNumber}/lock";
}

protected function defaultBody(): array
{
return $this->lockReason ? ['lock_reason' => $this->lockReason] : [];
}
}
24 changes: 24 additions & 0 deletions src/Requests/Issues/UnlockIssueRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Requests\Issues;

use Saloon\Enums\Method;
use Saloon\Http\Request;

class UnlockIssueRequest extends Request
{
protected Method $method = Method::DELETE;

public function __construct(
protected readonly string $owner,
protected readonly string $repo,
protected readonly int $issueNumber
) {}

public function resolveEndpoint(): string
{
return "/repos/{$this->owner}/{$this->repo}/issues/{$this->issueNumber}/lock";
}
}
269 changes: 269 additions & 0 deletions src/Services/IssueInstance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
<?php

declare(strict_types=1);

namespace ConduitUI\Issue\Services;

use ConduitUi\GitHubConnector\Connector;
use ConduitUI\Issue\Data\Comment;
use ConduitUI\Issue\Data\Issue;
use ConduitUI\Issue\Requests\Comments\CreateCommentRequest;
use ConduitUI\Issue\Requests\Issues\GetIssueRequest;
use ConduitUI\Issue\Requests\Issues\LockIssueRequest;
use ConduitUI\Issue\Requests\Issues\UnlockIssueRequest;
use ConduitUI\Issue\Requests\Issues\UpdateIssueRequest;
use ConduitUI\Issue\Requests\Labels\AddLabelsRequest;
use ConduitUI\Issue\Requests\Labels\RemoveLabelRequest;
use ConduitUI\Issue\Requests\Labels\ReplaceAllLabelsRequest;

final class IssueInstance
{
protected ?Issue $issue = null;

protected string $owner;

protected string $repo;

public function __construct(
protected Connector $connector,
string $fullName,
protected int $number,
) {
[$this->owner, $this->repo] = explode('/', $fullName, 2);
}

/**
* Get the issue data (cached)
*/
public function get(): Issue
{
if ($this->issue === null) {
$this->issue = $this->fetch();
}

return $this->issue;
}

/**
* Fetch fresh issue data
*/
public function fresh(): Issue
{
$this->issue = $this->fetch();

return $this->issue;
}

/**
* Update issue attributes
*/
public function update(array $attributes): self
{
$response = $this->connector->send(
new UpdateIssueRequest($this->owner, $this->repo, $this->number, $attributes)
);

$this->issue = Issue::fromArray($response->json());

return $this;
}

/**
* Set the title
*/
public function title(string $title): self
{
return $this->update(['title' => $title]);
}

/**
* Set the body
*/
public function body(string $body): self
{
return $this->update(['body' => $body]);
}

/**
* Add labels (merges with existing)
*/
public function addLabel(string $label): self
{
return $this->addLabels([$label]);
}

/**
* Add multiple labels
*/
public function addLabels(array $labels): self
{
$this->connector->send(
new AddLabelsRequest($this->owner, $this->repo, $this->number, $labels)
);

$this->issue = $this->fresh();

return $this;
}

/**
* Remove a label
*/
public function removeLabel(string $label): self
{
$this->connector->send(
new RemoveLabelRequest($this->owner, $this->repo, $this->number, $label)
);

$this->issue = $this->fresh();

return $this;
}

/**
* Remove multiple labels
*/
public function removeLabels(array $labels): self
{
foreach ($labels as $label) {
$this->removeLabel($label);
}

return $this;
}

/**
* Replace all labels
*/
public function setLabels(array $labels): self
{
$this->connector->send(
new ReplaceAllLabelsRequest($this->owner, $this->repo, $this->number, $labels)
);

$this->issue = $this->fresh();

return $this;
}

/**
* Assign to user(s)
*/
public function assign(string|array $assignees): self
{
$assignees = is_array($assignees) ? $assignees : [$assignees];

return $this->update(['assignees' => $assignees]);
}

/**
* Convenience method - assign to single user
*/
public function assignTo(string $username): self
{
return $this->assign($username);
}

/**
* Remove assignees
*/
public function unassign(string|array $assignees): self
{
$assignees = is_array($assignees) ? $assignees : [$assignees];

$current = array_map(fn ($user) => $user->login, $this->get()->assignees);
$remaining = array_diff($current, $assignees);

return $this->update(['assignees' => array_values($remaining)]);
}

/**
* Set milestone
*/
public function milestone(?int $milestoneNumber): self
{
return $this->update(['milestone' => $milestoneNumber]);
}

/**
* Close the issue
*/
public function close(?string $reason = null): self
{
$params = ['state' => 'closed'];

if ($reason !== null) {
$params['state_reason'] = $reason;
}

return $this->update($params);
}

/**
* Reopen the issue
*/
public function reopen(): self
{
return $this->update(['state' => 'open']);
}

/**
* Lock the issue
*/
public function lock(?string $reason = null): self
{
$this->connector->send(
new LockIssueRequest($this->owner, $this->repo, $this->number, $reason)
);

$this->issue = $this->fresh();

return $this;
}

/**
* Unlock the issue
*/
public function unlock(): self
{
$this->connector->send(
new UnlockIssueRequest($this->owner, $this->repo, $this->number)
);

$this->issue = $this->fresh();

return $this;
}

/**
* Add a comment
*/
public function comment(string $body): Comment
{
$response = $this->connector->send(
new CreateCommentRequest($this->owner, $this->repo, $this->number, $body)
);

return Comment::fromArray($response->json());
}

/**
* Fetch issue from API
*/
protected function fetch(): Issue
{
$response = $this->connector->send(
new GetIssueRequest($this->owner, $this->repo, $this->number)
);

return Issue::fromArray($response->json());
}

/**
* Magic method to access issue properties
*/
public function __get(string $name): mixed
{
return $this->get()->$name;
}
}
Loading
Loading