user(); abort_unless( $this->permissionService->allows($user, Permission::ProjectRead, $project), 403, ); $canEditTicket = $this->permissionService->allows($user, Permission::ProjectWrite, $project); $canManageProject = $this->permissionService->allows($user, Permission::ProjectAdmin, $project); $excludedParentIds = [$ticket->id, ...$this->descendantIds($ticket)]; $displayTicket = $ticket->load([ 'assignee', 'author', 'fieldValues.ticketFieldDefinition', 'fieldValues.userValue', 'parentTicket', 'ticketPriority', 'ticketStatus.boardColumn', 'ticketType', 'worklogEntries.actor', ]); if (! $project->show_author && ! $canManageProject) { $displayTicket->setRelation('author', null); $displayTicket->author_user_id = null; } return Inertia::render('projects/Ticket', [ 'canEditTicket' => $canEditTicket, 'canManageProject' => $canManageProject, 'childTicketTree' => $this->buildChildTicketTree($ticket), 'linkedTickets' => $this->linkedTickets($ticket), 'parentTicketOptions' => Ticket::query() ->where('project_id', $project->id) ->whereNotIn('id', $excludedParentIds) ->whereHas('ticketType', fn ($query) => $query->where('is_parent', true)) ->orderByDesc('sequence_number') ->get() ->map(fn (Ticket $parentTicket): array => [ 'id' => $parentTicket->id, 'ticket_key' => $parentTicket->ticket_key, 'title' => $parentTicket->title, ]), 'project' => $project, 'repositoryBranches' => $this->resolveTicketBranchMatches->handle( $user, $project, $ticket, ), 'repositoryPullRequests' => $this->resolveTicketPullRequestMatches->handle( $user, $project, $ticket, ), 'status' => session('status'), 'suggestedBranchName' => $this->buildSuggestedTicketBranchName->handle($ticket), 'ticket' => $displayTicket, 'ticketFieldDefinitions' => $project->ticketFieldDefinitions()->get(), 'ticketLinkOptions' => Ticket::query() ->where('project_id', $project->id) ->whereKeyNot($ticket->id) ->orderByDesc('sequence_number') ->get() ->map(fn (Ticket $linkedTicket): array => [ 'id' => $linkedTicket->id, 'ticket_key' => $linkedTicket->ticket_key, 'title' => $linkedTicket->title, ]), 'ticketLinkTypes' => $project->ticketLinkTypes()->get(), 'ticketPriorities' => $project->ticketPriorities()->get(), 'ticketTypes' => $project->ticketTypes()->get(), 'users' => User::query() ->orderBy('name') ->get(), ]); } public function update( UpdateProjectTicketRequest $request, Project $project, Ticket $ticket, ): RedirectResponse { $user = $request->user(); abort_unless( $this->permissionService->allows($user, Permission::ProjectWrite, $project), 403, ); $validated = $request->validated(); $fieldValues = Arr::get($validated, 'field_values', []); $ticketAttributes = [ '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'), ]; $wasUpdated = $this->updateProjectTicket->handle( $ticket, $user, $ticketAttributes, $fieldValues, ); $statusMessage = $wasUpdated ? "{$ticket->ticket_key} was updated." : 'No ticket changes were detected.'; return to_route('projects.tickets.show', [$project, $ticket]) ->with('status', $statusMessage); } /** * @return array, * id: int, * ticket_key: string, * ticket_type_name: string|null, * title: string * }> */ private function buildChildTicketTree(Ticket $ticket): array { return $ticket->childTickets() ->with('ticketType') ->get() ->map(fn (Ticket $childTicket): array => [ 'children' => $this->buildChildTicketTree($childTicket), 'id' => $childTicket->id, 'ticket_key' => $childTicket->ticket_key, 'ticket_type_name' => $childTicket->ticketType?->name, 'title' => $childTicket->title, ]) ->all(); } /** * @return array */ private function descendantIds(Ticket $ticket): array { return $ticket->childTickets() ->get() ->flatMap(function (Ticket $childTicket): array { return [$childTicket->id, ...$this->descendantIds($childTicket)]; }) ->values() ->all(); } /** * @return array */ private function linkedTickets(Ticket $ticket): array { return TicketLink::query() ->with(['sourceTicket', 'targetTicket', 'ticketLinkType']) ->where(function ($query) use ($ticket): void { $query ->where('source_ticket_id', $ticket->id) ->orWhere('target_ticket_id', $ticket->id); }) ->orderBy('id') ->get() ->map(function (TicketLink $ticketLink) use ($ticket): array { $linkedTicket = $ticketLink->source_ticket_id === $ticket->id ? $ticketLink->targetTicket : $ticketLink->sourceTicket; return [ 'created_at' => $ticketLink->created_at?->toISOString(), 'id' => $ticketLink->id, 'linked_ticket' => [ 'id' => $linkedTicket->id, 'ticket_key' => $linkedTicket->ticket_key, 'title' => $linkedTicket->title, ], 'source_ticket_id' => $ticketLink->source_ticket_id, 'target_ticket_id' => $ticketLink->target_ticket_id, 'ticket_link_type' => $ticketLink->ticketLinkType, 'ticket_link_type_id' => $ticketLink->ticket_link_type_id, 'updated_at' => $ticketLink->updated_at?->toISOString(), ]; }) ->all(); } }