Implement project board status workflow
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Tickets;
|
||||
|
||||
use App\Models\Ticket;
|
||||
use App\Models\TicketWorklogEntry;
|
||||
use App\Models\User;
|
||||
|
||||
class CreateTicketWorklogEntry
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed>|null $changes
|
||||
*/
|
||||
public function handle(
|
||||
Ticket $ticket,
|
||||
?User $actor,
|
||||
string $entryType,
|
||||
string $summary,
|
||||
?array $changes = null,
|
||||
): TicketWorklogEntry {
|
||||
return $ticket->worklogEntries()->create([
|
||||
'actor_id' => $actor?->id,
|
||||
'changes' => $changes,
|
||||
'entry_type' => $entryType,
|
||||
'summary' => $summary,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ class ProjectBacklogController extends Controller
|
||||
);
|
||||
|
||||
return Inertia::render('projects/Backlog', [
|
||||
'boardColumns' => $project->boardColumns()->get(),
|
||||
'canEditTickets' => $this->projectPermissionService->userHasPermission($request->user(), $project, 'edit'),
|
||||
'canCreateTickets' => $this->projectPermissionService->userHasPermission($request->user(), $project, 'create'),
|
||||
'project' => $project,
|
||||
'status' => session('status'),
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\ProjectPermissions\ProjectPermissionService;
|
||||
use App\Actions\Tickets\CreateTicketWorklogEntry;
|
||||
use App\Http\Requests\MoveProjectTicketRequest;
|
||||
use App\Models\Project;
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProjectBoardController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CreateTicketWorklogEntry $createTicketWorklogEntry,
|
||||
private readonly ProjectPermissionService $projectPermissionService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request, Project $project): Response
|
||||
{
|
||||
abort_unless(
|
||||
$this->projectPermissionService->userCanViewProject($request->user(), $project),
|
||||
403,
|
||||
);
|
||||
|
||||
return Inertia::render('projects/Board', [
|
||||
'boardColumns' => $project->boardColumns()
|
||||
->with([
|
||||
'tickets' => fn ($query) => $query
|
||||
->with(['ticketPriority', 'ticketType']),
|
||||
])
|
||||
->get(),
|
||||
'canEditTickets' => $this->projectPermissionService->userHasPermission($request->user(), $project, 'edit'),
|
||||
'project' => $project,
|
||||
'status' => session('status'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function move(
|
||||
MoveProjectTicketRequest $request,
|
||||
Project $project,
|
||||
Ticket $ticket,
|
||||
): RedirectResponse {
|
||||
abort_unless(
|
||||
$this->projectPermissionService->userHasPermission($request->user(), $project, 'edit'),
|
||||
403,
|
||||
);
|
||||
|
||||
$destinationColumn = $project->boardColumns()
|
||||
->find($request->boardColumnId());
|
||||
|
||||
$originBoardColumnId = $ticket->board_column_id;
|
||||
$originLabel = $ticket->boardColumn?->name ?? 'Backlog';
|
||||
$destinationLabel = $destinationColumn?->name ?? 'Backlog';
|
||||
|
||||
if ($ticket->board_column_id === $request->boardColumnId()) {
|
||||
return to_route($request->redirectRouteName(), $project)
|
||||
->with('status', "{$ticket->ticket_key} already belongs to {$destinationLabel}.");
|
||||
}
|
||||
|
||||
$ticket->update([
|
||||
'board_column_id' => $destinationColumn?->id,
|
||||
]);
|
||||
|
||||
$this->createTicketWorklogEntry->handle(
|
||||
$ticket->fresh(),
|
||||
$request->user(),
|
||||
'status_changed',
|
||||
"Moved from {$originLabel} to {$destinationLabel}.",
|
||||
[
|
||||
'from' => [
|
||||
'board_column_id' => $originBoardColumnId,
|
||||
'label' => $originLabel,
|
||||
],
|
||||
'to' => [
|
||||
'board_column_id' => $destinationColumn?->id,
|
||||
'label' => $destinationLabel,
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
return to_route($request->redirectRouteName(), $project)
|
||||
->with('status', "{$ticket->ticket_key} moved to {$destinationLabel}.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Project;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class MoveProjectTicketRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
|
||||
return [
|
||||
'board_column_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('board_columns', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
'redirect_to' => ['required', 'string', Rule::in(['backlog', 'board'])],
|
||||
];
|
||||
}
|
||||
|
||||
public function boardColumnId(): ?int
|
||||
{
|
||||
$boardColumnId = $this->validated('board_column_id');
|
||||
|
||||
if ($boardColumnId === null || $boardColumnId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $boardColumnId;
|
||||
}
|
||||
|
||||
public function redirectRouteName(): string
|
||||
{
|
||||
return $this->validated('redirect_to') === 'backlog'
|
||||
? 'projects.backlog.index'
|
||||
: 'projects.board.index';
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['name', 'position', 'project_id'])]
|
||||
class BoardColumn extends Model
|
||||
@@ -18,4 +19,10 @@ class BoardColumn extends Model
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
public function tickets(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ticket::class)
|
||||
->orderByDesc('sequence_number');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable([
|
||||
'acceptance_criteria',
|
||||
@@ -43,4 +44,11 @@ class Ticket extends Model
|
||||
{
|
||||
return $this->belongsTo(TicketType::class);
|
||||
}
|
||||
|
||||
public function worklogEntries(): HasMany
|
||||
{
|
||||
return $this->hasMany(TicketWorklogEntry::class)
|
||||
->orderByDesc('created_at')
|
||||
->orderByDesc('id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\TicketWorklogEntryFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable([
|
||||
'actor_id',
|
||||
'changes',
|
||||
'entry_type',
|
||||
'summary',
|
||||
'ticket_id',
|
||||
])]
|
||||
class TicketWorklogEntry extends Model
|
||||
{
|
||||
/** @use HasFactory<TicketWorklogEntryFactory> */
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'changes' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function actor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'actor_id');
|
||||
}
|
||||
|
||||
public function ticket(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Ticket::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user