fixed missing features

This commit is contained in:
2026-04-15 09:56:53 +02:00
parent de461ad73a
commit cec0e397b6
44 changed files with 1102 additions and 561 deletions
@@ -30,20 +30,16 @@ interface GitProviderClient
* @return array<int, GitBranch>
*/
public function listBranches(
string $baseUrl,
string $token,
string $owner,
string $repository,
GitRepository $repository,
): array;
/**
* @return array<int, GitPullRequest>
*/
public function listPullRequests(
string $baseUrl,
string $token,
string $owner,
string $repository,
GitRepository $repository,
): array;
public function createBranch(
+1
View File
@@ -8,6 +8,7 @@ namespace App\Actions\Git\Data;
final readonly class GitRepository
{
public function __construct(
public string $baseUrl,
public string $providerRepositoryId,
public string $owner,
public string $name,
+10 -13
View File
@@ -55,7 +55,7 @@ class GiteaProviderClient implements GitProviderClient
->throw()
->json();
return $this->mapRepository($payload);
return $this->mapRepository($payload, $baseUrl);
}
/**
@@ -85,7 +85,7 @@ class GiteaProviderClient implements GitProviderClient
->json();
foreach ($payload as $repository) {
$repositories[] = $this->mapRepository($repository);
$repositories[] = $this->mapRepository($repository, $baseUrl);
}
$page++;
@@ -98,19 +98,17 @@ class GiteaProviderClient implements GitProviderClient
* @return array<int, GitBranch>
*/
public function listBranches(
string $baseUrl,
string $token,
string $owner,
string $repository,
GitRepository $repository,
): array {
$page = 1;
$branches = [];
do {
/** @var array<int, array{name: string}> $payload */
$payload = $this->request($baseUrl, $token)
$payload = $this->request($repository->baseUrl, $token)
->get(
'/repos/'.rawurlencode($owner).'/'.rawurlencode($repository).'/branches',
'/repos/'.rawurlencode($repository->owner).'/'.rawurlencode($repository->name).'/branches',
[
'limit' => 100,
'page' => $page,
@@ -135,10 +133,8 @@ class GiteaProviderClient implements GitProviderClient
* @return array<int, GitPullRequest>
*/
public function listPullRequests(
string $baseUrl,
string $token,
string $owner,
string $repository,
GitRepository $repository,
): array {
$page = 1;
$pullRequests = [];
@@ -153,9 +149,9 @@ class GiteaProviderClient implements GitProviderClient
* head: array{ref: string}
* }> $payload
*/
$payload = $this->request($baseUrl, $token)
$payload = $this->request($repository->baseUrl, $token)
->get(
'/repos/'.rawurlencode($owner).'/'.rawurlencode($repository).'/pulls',
'/repos/'.rawurlencode($repository->owner).'/'.rawurlencode($repository->name).'/pulls',
[
'limit' => 100,
'page' => $page,
@@ -248,9 +244,10 @@ class GiteaProviderClient implements GitProviderClient
* private: bool
* } $repository
*/
private function mapRepository(array $repository): GitRepository
private function mapRepository(array $repository, string $baseUrl): GitRepository
{
return new GitRepository(
baseUrl: $baseUrl,
providerRepositoryId: (string) $repository['id'],
owner: $repository['owner']['login'],
name: $repository['name'],
@@ -66,10 +66,8 @@ class ResolveTicketBranchMatches
$this->gitProviderManager
->client($userGitConnection->provider)
->listBranches(
$userGitConnection->base_url,
$userGitConnection->api_token,
$repositoryConnection->owner_name,
$repositoryConnection->repository_name,
$repositoryConnection->toGitRepository(),
),
)
->filter(fn (GitBranch $branch): bool => $this->ticketKeyReferenceMatcher->matches(
@@ -85,7 +83,7 @@ class ResolveTicketBranchMatches
$error = null;
} catch (Throwable) {
$branches = [];
$error = 'KanbanGit could not load branches from this repository right now.';
$error = ((string) config('app.name')).' could not load branches from this repository right now.';
}
return [
@@ -72,10 +72,8 @@ class ResolveTicketPullRequestMatches
$this->gitProviderManager
->client($userGitConnection->provider)
->listPullRequests(
$userGitConnection->base_url,
$userGitConnection->api_token,
$repositoryConnection->owner_name,
$repositoryConnection->repository_name,
$repositoryConnection->toGitRepository(),
),
)
->filter(fn (GitPullRequest $pullRequest): bool => $this->ticketKeyReferenceMatcher->matches(
@@ -94,7 +92,7 @@ class ResolveTicketPullRequestMatches
$error = null;
} catch (Throwable) {
$error = 'KanbanGit could not load pull requests from this repository right now.';
$error = ((string) config('app.name')).' could not load pull requests from this repository right now.';
$pullRequests = [];
}
@@ -0,0 +1,49 @@
<?php
namespace App\Actions\Projects;
use App\Models\Project;
use App\Models\Ticket;
use App\Models\User;
class BuildProjectBacklogPageProps
{
/**
* @return array<string, mixed>
*/
public function handle(
Project $project,
bool $canEditTickets,
bool $canCreateTickets,
?string $status,
): array {
return [
'backlogTicketStatuses' => $project->ticketStatuses()
->whereNull('board_column_id')
->get(),
'canCreateTickets' => $canCreateTickets,
'canEditTickets' => $canEditTickets,
'project' => $project,
'status' => $status,
'ticketStatusOptions' => $project->ticketStatuses()
->with('boardColumn')
->get(),
'ticketPriorities' => $project->ticketPriorities()->get(),
'ticketTypes' => $project->ticketTypes()->get(),
'tickets' => Ticket::query()
->with([
'ticketPriority',
'ticketStatus.allowedTargetStatuses.boardColumn',
'ticketStatus.boardColumn',
'ticketType',
])
->where('project_id', $project->id)
->whereHas('ticketStatus', fn ($query) => $query->whereNull('board_column_id'))
->orderByDesc('sequence_number')
->get(),
'users' => User::query()
->orderBy('name')
->get(),
];
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Actions\Projects;
use App\Models\Project;
class BuildProjectBoardPageProps
{
/**
* @return array<string, mixed>
*/
public function handle(
Project $project,
bool $canEditTickets,
bool $canCreateTickets,
?string $status,
): array {
return [
'boardColumns' => $project->boardColumns()
->with([
'ticketStatuses',
'tickets' => fn ($query) => $query
->with([
'ticketPriority',
'ticketStatus.allowedTargetStatuses.boardColumn',
'ticketStatus.boardColumn',
'ticketType',
]),
])
->get(),
'canCreateTickets' => $canCreateTickets,
'canEditTickets' => $canEditTickets,
'project' => $project,
'status' => $status,
];
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Actions\Tickets;
use App\Models\Ticket;
use Illuminate\Support\Arr;
class BuildProjectTicketUpdateAttributes
{
/**
* @param array<string, mixed> $validated
* @return array{
* acceptance_criteria: string|null,
* assignee_user_id: int|null,
* author_user_id: int|null,
* blocked_comment: string|null,
* description: string|null,
* is_blocked: bool,
* parent_ticket_id: int|null,
* story_points: int|null,
* ticket_priority_id: int,
* ticket_type_id: int,
* title: string
* }
*/
public function handle(Ticket $ticket, array $validated): array
{
return [
'acceptance_criteria' => Arr::get($validated, 'acceptance_criteria'),
'assignee_user_id' => Arr::exists($validated, 'assignee_user_id')
? Arr::get($validated, 'assignee_user_id')
: $ticket->assignee_user_id,
'author_user_id' => Arr::exists($validated, 'author_user_id')
? Arr::get($validated, 'author_user_id')
: $ticket->author_user_id,
'blocked_comment' => Arr::get($validated, 'blocked_comment'),
'description' => Arr::get($validated, 'description'),
'is_blocked' => Arr::get($validated, 'is_blocked', false),
'parent_ticket_id' => Arr::get($validated, 'parent_ticket_id'),
'story_points' => Arr::exists($validated, 'story_points')
? Arr::get($validated, 'story_points')
: $ticket->story_points,
'ticket_priority_id' => Arr::get($validated, 'ticket_priority_id'),
'ticket_type_id' => Arr::get($validated, 'ticket_type_id'),
'title' => Arr::get($validated, 'title'),
];
}
}
+2 -1
View File
@@ -2,6 +2,7 @@
namespace App\Actions\Tickets;
use App\Enums\TicketWorklogEntryType;
use App\Models\Project;
use App\Models\Ticket;
use App\Models\TicketStatus;
@@ -56,7 +57,7 @@ class CreateProjectTicket
$this->createTicketWorklogEntry->handle(
$ticket,
$actor,
'created',
TicketWorklogEntryType::Created,
"Ticket was created in {$ticketStatus->name}.",
[
'from' => null,
@@ -2,6 +2,7 @@
namespace App\Actions\Tickets;
use App\Enums\TicketWorklogEntryType;
use App\Models\Ticket;
use App\Models\TicketWorklogEntry;
use App\Models\User;
@@ -14,7 +15,7 @@ class CreateTicketWorklogEntry
public function handle(
Ticket $ticket,
?User $actor,
string $entryType,
TicketWorklogEntryType $entryType,
string $summary,
?array $changes = null,
): TicketWorklogEntry {
@@ -0,0 +1,173 @@
<?php
namespace App\Actions\Tickets;
use App\Models\Ticket;
use App\Models\TicketFieldDefinition;
use App\Models\TicketFieldValue;
use App\Models\User;
use Illuminate\Support\Collection;
class SyncProjectTicketFieldValues
{
/**
* @param Collection<int, TicketFieldDefinition> $fieldDefinitions
* @param Collection<int, TicketFieldValue> $existingFieldValues
* @param array<int, array{
* ticket_field_definition_id: int,
* value_number?: float|int|string|null,
* value_user_id?: int|string|null
* }> $fieldValueAttributes
* @return array{
* changes: array<int, array<string, mixed>>,
* summary_labels: array<int, string>
* }
*/
public function handle(
Ticket $ticket,
Collection $fieldDefinitions,
Collection $existingFieldValues,
array $fieldValueAttributes,
): array {
$changes = [];
$summaryLabels = [];
foreach ($fieldValueAttributes as $fieldValueAttribute) {
$fieldDefinition = $fieldDefinitions->get((int) $fieldValueAttribute['ticket_field_definition_id']);
if ($fieldDefinition === null) {
continue;
}
$existingFieldValue = $existingFieldValues->get($fieldDefinition->id);
if ($fieldDefinition->field_type === 'user') {
$this->syncUserFieldValue(
$changes,
$existingFieldValue,
$fieldDefinition,
$fieldValueAttribute,
$summaryLabels,
$ticket,
);
continue;
}
$this->syncNumberFieldValue(
$changes,
$existingFieldValue,
$fieldDefinition,
$fieldValueAttribute,
$summaryLabels,
$ticket,
);
}
return [
'changes' => $changes,
'summary_labels' => $summaryLabels,
];
}
/**
* @param array<int, array<string, mixed>> $changes
* @param array{value_user_id?: int|string|null} $fieldValueAttribute
* @param array<int, string> $summaryLabels
*/
private function syncUserFieldValue(
array &$changes,
?TicketFieldValue $existingFieldValue,
TicketFieldDefinition $fieldDefinition,
array $fieldValueAttribute,
array &$summaryLabels,
Ticket $ticket,
): void {
$newUserId = $fieldValueAttribute['value_user_id'] ?? null;
$newUserId = $newUserId === null || $newUserId === '' ? null : (int) $newUserId;
if ($existingFieldValue?->value_user_id === $newUserId) {
return;
}
$oldUserName = $existingFieldValue?->userValue?->name;
$newUserName = $newUserId === null ? null : User::query()->find($newUserId)?->name;
$changes[$fieldDefinition->id] = [
'field' => $fieldDefinition->label,
'from' => $oldUserName,
'to' => $newUserName,
];
$summaryLabels[] = "{$fieldDefinition->label} field";
if ($newUserId === null) {
$existingFieldValue?->delete();
return;
}
$ticket->fieldValues()->updateOrCreate(
[
'ticket_field_definition_id' => $fieldDefinition->id,
],
[
'value_number' => null,
'value_user_id' => $newUserId,
],
);
}
/**
* @param array<int, array<string, mixed>> $changes
* @param array{value_number?: float|int|string|null} $fieldValueAttribute
* @param array<int, string> $summaryLabels
*/
private function syncNumberFieldValue(
array &$changes,
?TicketFieldValue $existingFieldValue,
TicketFieldDefinition $fieldDefinition,
array $fieldValueAttribute,
array &$summaryLabels,
Ticket $ticket,
): void {
$newNumber = $fieldValueAttribute['value_number'] ?? null;
$newNumber = $newNumber === null || $newNumber === '' ? null : (float) $newNumber;
$oldNumber = $existingFieldValue?->value_number;
if ($oldNumber === $newNumber) {
return;
}
$changes[$fieldDefinition->id] = [
'field' => $fieldDefinition->label,
'from' => $this->formatNumber($oldNumber),
'to' => $this->formatNumber($newNumber),
];
$summaryLabels[] = "{$fieldDefinition->label} field";
if ($newNumber === null) {
$existingFieldValue?->delete();
return;
}
$ticket->fieldValues()->updateOrCreate(
[
'ticket_field_definition_id' => $fieldDefinition->id,
],
[
'value_number' => $newNumber,
'value_user_id' => null,
],
);
}
private function formatNumber(?float $value): ?string
{
if ($value === null) {
return null;
}
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
}
}
+15 -131
View File
@@ -2,6 +2,7 @@
namespace App\Actions\Tickets;
use App\Enums\TicketWorklogEntryType;
use App\Models\Ticket;
use App\Models\TicketFieldDefinition;
use App\Models\TicketFieldValue;
@@ -13,6 +14,7 @@ class UpdateProjectTicket
{
public function __construct(
private readonly CreateTicketWorklogEntry $createTicketWorklogEntry,
private readonly SyncProjectTicketFieldValues $syncProjectTicketFieldValues,
) {}
/**
@@ -179,36 +181,19 @@ class UpdateProjectTicket
$ticket->save();
}
foreach ($fieldValueAttributes as $fieldValueAttribute) {
$fieldDefinition = $fieldDefinitions->get((int) $fieldValueAttribute['ticket_field_definition_id']);
$fieldValueChangeSet = $this->syncProjectTicketFieldValues->handle(
$ticket,
$fieldDefinitions,
$existingFieldValues,
$fieldValueAttributes,
);
if ($fieldDefinition === null) {
continue;
}
$existingFieldValue = $existingFieldValues->get($fieldDefinition->id);
if ($fieldDefinition->field_type === 'user') {
$this->syncUserFieldValue(
$changes,
$existingFieldValue,
$fieldDefinition,
$fieldValueAttribute,
$summaryLabels,
$ticket,
);
continue;
}
$this->syncNumberFieldValue(
$changes,
$existingFieldValue,
$fieldDefinition,
$fieldValueAttribute,
$summaryLabels,
$ticket,
);
if ($fieldValueChangeSet['changes'] !== []) {
$changes['field_values'] = $fieldValueChangeSet['changes'];
$summaryLabels = [
...$summaryLabels,
...$fieldValueChangeSet['summary_labels'],
];
}
if ($summaryLabels === []) {
@@ -218,7 +203,7 @@ class UpdateProjectTicket
$this->createTicketWorklogEntry->handle(
$ticket->fresh(),
$actor,
'updated',
TicketWorklogEntryType::Updated,
$this->buildSummary($summaryLabels),
$changes,
);
@@ -273,98 +258,6 @@ class UpdateProjectTicket
$summaryLabels[] = $summaryLabel;
}
/**
* @param array<string, mixed> $changes
* @param array{value_user_id?: int|string|null} $fieldValueAttribute
* @param array<int, string> $summaryLabels
*/
private function syncUserFieldValue(
array &$changes,
?TicketFieldValue $existingFieldValue,
TicketFieldDefinition $fieldDefinition,
array $fieldValueAttribute,
array &$summaryLabels,
Ticket $ticket,
): void {
$newUserId = $fieldValueAttribute['value_user_id'] ?? null;
$newUserId = $newUserId === null || $newUserId === '' ? null : (int) $newUserId;
if ($existingFieldValue?->value_user_id === $newUserId) {
return;
}
$oldUserName = $existingFieldValue?->userValue?->name;
$newUserName = $newUserId === null ? null : User::query()->find($newUserId)?->name;
$changes['field_values'][$fieldDefinition->id] = [
'field' => $fieldDefinition->label,
'from' => $oldUserName,
'to' => $newUserName,
];
$summaryLabels[] = "{$fieldDefinition->label} field";
if ($newUserId === null) {
$existingFieldValue?->delete();
return;
}
$ticket->fieldValues()->updateOrCreate(
[
'ticket_field_definition_id' => $fieldDefinition->id,
],
[
'value_number' => null,
'value_user_id' => $newUserId,
],
);
}
/**
* @param array<string, mixed> $changes
* @param array{value_number?: float|int|string|null} $fieldValueAttribute
* @param array<int, string> $summaryLabels
*/
private function syncNumberFieldValue(
array &$changes,
?TicketFieldValue $existingFieldValue,
TicketFieldDefinition $fieldDefinition,
array $fieldValueAttribute,
array &$summaryLabels,
Ticket $ticket,
): void {
$newNumber = $fieldValueAttribute['value_number'] ?? null;
$newNumber = $newNumber === null || $newNumber === '' ? null : (float) $newNumber;
$oldNumber = $existingFieldValue?->value_number;
if ($oldNumber === $newNumber) {
return;
}
$changes['field_values'][$fieldDefinition->id] = [
'field' => $fieldDefinition->label,
'from' => $this->formatNumber($oldNumber),
'to' => $this->formatNumber($newNumber),
];
$summaryLabels[] = "{$fieldDefinition->label} field";
if ($newNumber === null) {
$existingFieldValue?->delete();
return;
}
$ticket->fieldValues()->updateOrCreate(
[
'ticket_field_definition_id' => $fieldDefinition->id,
],
[
'value_number' => $newNumber,
'value_user_id' => null,
],
);
}
/**
* @param array<int, string> $summaryLabels
*/
@@ -386,13 +279,4 @@ class UpdateProjectTicket
return 'Updated '.$uniqueLabels->implode(', ').', and '.$lastLabel.'.';
}
private function formatNumber(?float $value): ?string
{
if ($value === null) {
return null;
}
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
}
}