Implement ticket statuses
This commit is contained in:
@@ -5,6 +5,7 @@ namespace Database\Factories;
|
||||
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
|
||||
use App\Models\Project;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\TicketStatus;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
@@ -24,7 +25,6 @@ class TicketFactory extends Factory
|
||||
'acceptance_criteria' => fake()->paragraph(),
|
||||
'assignee_user_id' => null,
|
||||
'author_user_id' => User::factory(),
|
||||
'board_column_id' => null,
|
||||
'blocked_comment' => null,
|
||||
'description' => fake()->paragraphs(2, true),
|
||||
'is_blocked' => false,
|
||||
@@ -33,27 +33,49 @@ class TicketFactory extends Factory
|
||||
'story_points' => null,
|
||||
'ticket_key' => 'TEST-1',
|
||||
'title' => fake()->sentence(4),
|
||||
'ticket_status_id' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function configure(): static
|
||||
{
|
||||
return $this->afterMaking(function (Ticket $ticket): void {
|
||||
if ($ticket->project_id !== null && $ticket->ticket_type_id !== null && $ticket->ticket_priority_id !== null) {
|
||||
if (
|
||||
$ticket->project_id !== null
|
||||
&& $ticket->ticket_type_id !== null
|
||||
&& $ticket->ticket_priority_id !== null
|
||||
&& $ticket->ticket_status_id !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$project = Project::factory()->create([
|
||||
'key' => fake()->unique()->lexify('????'),
|
||||
]);
|
||||
$createdProject = $ticket->project_id === null;
|
||||
$project = $createdProject
|
||||
? Project::factory()->create([
|
||||
'key' => fake()->unique()->lexify('????'),
|
||||
])
|
||||
: Project::query()->findOrFail($ticket->project_id);
|
||||
|
||||
app(InitializeProjectTicketMetadata::class)->handle($project);
|
||||
|
||||
$ticket->project_id = $project->id;
|
||||
$ticket->ticket_type_id = $project->ticketTypes()->firstOrFail()->id;
|
||||
$ticket->ticket_priority_id = $project->ticketPriorities()->firstOrFail()->id;
|
||||
$ticket->sequence_number = 1;
|
||||
$ticket->ticket_key = "{$project->key}-1";
|
||||
$ticket->ticket_type_id ??= $project->ticketTypes()->firstOrFail()->id;
|
||||
$ticket->ticket_priority_id ??= $project->ticketPriorities()->firstOrFail()->id;
|
||||
$ticket->ticket_status_id ??= $project->ticketStatuses()
|
||||
->whereNull('board_column_id')
|
||||
->first()?->id
|
||||
?? TicketStatus::factory()->for($project)->create([
|
||||
'name' => 'Backlog',
|
||||
'position' => 1,
|
||||
])->id;
|
||||
|
||||
if ($createdProject || $ticket->sequence_number === null) {
|
||||
$ticket->sequence_number = 1;
|
||||
}
|
||||
|
||||
if ($createdProject || $ticket->ticket_key === null) {
|
||||
$ticket->ticket_key = "{$project->key}-{$ticket->sequence_number}";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\TicketStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<TicketStatus>
|
||||
*/
|
||||
class TicketStatusFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'board_column_id' => null,
|
||||
'name' => fake()->unique()->words(2, true),
|
||||
'position' => 1,
|
||||
'project_id' => Project::factory(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ticket_statuses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('board_column_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('slug');
|
||||
$table->unsignedInteger('position');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['project_id', 'board_column_id', 'position']);
|
||||
$table->index(['project_id', 'slug']);
|
||||
});
|
||||
|
||||
Schema::create('ticket_status_transitions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('from_ticket_status_id')->constrained('ticket_statuses')->cascadeOnDelete();
|
||||
$table->foreignId('to_ticket_status_id')->constrained('ticket_statuses')->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(
|
||||
['from_ticket_status_id', 'to_ticket_status_id'],
|
||||
'ticket_status_transitions_unique',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ticket_status_transitions');
|
||||
Schema::dropIfExists('ticket_statuses');
|
||||
}
|
||||
};
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('tickets', function (Blueprint $table) {
|
||||
$table->foreignId('ticket_status_id')
|
||||
->nullable()
|
||||
->after('board_column_id')
|
||||
->constrained()
|
||||
->nullOnDelete();
|
||||
});
|
||||
|
||||
$this->backfillTicketStatuses();
|
||||
|
||||
Schema::table('tickets', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('board_column_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tickets', function (Blueprint $table) {
|
||||
$table->foreignId('board_column_id')
|
||||
->nullable()
|
||||
->after('author_user_id')
|
||||
->constrained()
|
||||
->nullOnDelete();
|
||||
});
|
||||
|
||||
DB::table('tickets')
|
||||
->leftJoin('ticket_statuses', 'tickets.ticket_status_id', '=', 'ticket_statuses.id')
|
||||
->update([
|
||||
'tickets.board_column_id' => DB::raw('ticket_statuses.board_column_id'),
|
||||
]);
|
||||
|
||||
Schema::table('tickets', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('ticket_status_id');
|
||||
});
|
||||
}
|
||||
|
||||
private function backfillTicketStatuses(): void
|
||||
{
|
||||
DB::table('projects')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->each(function (object $project): void {
|
||||
$backlogStatusId = $this->createTicketStatus(
|
||||
(int) $project->id,
|
||||
null,
|
||||
'Backlog',
|
||||
1,
|
||||
);
|
||||
|
||||
DB::table('tickets')
|
||||
->where('project_id', $project->id)
|
||||
->whereNull('board_column_id')
|
||||
->update(['ticket_status_id' => $backlogStatusId]);
|
||||
|
||||
DB::table('board_columns')
|
||||
->where('project_id', $project->id)
|
||||
->orderBy('position')
|
||||
->get()
|
||||
->each(function (object $boardColumn) use ($project): void {
|
||||
$ticketStatusId = $this->createTicketStatus(
|
||||
(int) $project->id,
|
||||
(int) $boardColumn->id,
|
||||
(string) $boardColumn->name,
|
||||
1,
|
||||
);
|
||||
|
||||
DB::table('tickets')
|
||||
->where('project_id', $project->id)
|
||||
->where('board_column_id', $boardColumn->id)
|
||||
->update(['ticket_status_id' => $ticketStatusId]);
|
||||
});
|
||||
|
||||
$this->createFullTransitionMatrix((int) $project->id);
|
||||
});
|
||||
}
|
||||
|
||||
private function createTicketStatus(
|
||||
int $projectId,
|
||||
?int $boardColumnId,
|
||||
string $name,
|
||||
int $position,
|
||||
): int {
|
||||
$now = now();
|
||||
|
||||
return (int) DB::table('ticket_statuses')->insertGetId([
|
||||
'board_column_id' => $boardColumnId,
|
||||
'created_at' => $now,
|
||||
'name' => $name,
|
||||
'position' => $position,
|
||||
'project_id' => $projectId,
|
||||
'slug' => Str::slug($name),
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createFullTransitionMatrix(int $projectId): void
|
||||
{
|
||||
$now = now();
|
||||
$statusIds = DB::table('ticket_statuses')
|
||||
->where('project_id', $projectId)
|
||||
->pluck('id')
|
||||
->map(fn (int|string $statusId): int => (int) $statusId);
|
||||
|
||||
$statusIds->each(function (int $fromStatusId) use ($now, $statusIds): void {
|
||||
$statusIds->each(function (int $toStatusId) use ($fromStatusId, $now): void {
|
||||
DB::table('ticket_status_transitions')->insertOrIgnore([
|
||||
'created_at' => $now,
|
||||
'from_ticket_status_id' => $fromStatusId,
|
||||
'to_ticket_status_id' => $toStatusId,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -25,7 +25,14 @@ class BoardColumnSeeder extends Seeder
|
||||
['name' => 'Done', 'position' => 3],
|
||||
)
|
||||
->for($project)
|
||||
->create();
|
||||
->create()
|
||||
->each(function (BoardColumn $boardColumn): void {
|
||||
$boardColumn->ticketStatuses()->create([
|
||||
'name' => $boardColumn->name,
|
||||
'position' => 1,
|
||||
'project_id' => $boardColumn->project_id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class DatabaseSeeder extends Seeder
|
||||
RepositoryConnectionSeeder::class,
|
||||
BoardColumnSeeder::class,
|
||||
ProjectTicketMetadataSeeder::class,
|
||||
TicketStatusSeeder::class,
|
||||
TicketSeeder::class,
|
||||
TicketFieldValueSeeder::class,
|
||||
TicketLinkTypeSeeder::class,
|
||||
|
||||
@@ -37,6 +37,9 @@ class TicketSeeder extends Seeder
|
||||
}
|
||||
|
||||
$defaultType = $project->ticketTypes()->firstOrFail();
|
||||
$defaultStatus = $project->ticketStatuses()
|
||||
->whereNull('board_column_id')
|
||||
->firstOrFail();
|
||||
|
||||
$createProjectTicket->handle($project, [
|
||||
'acceptance_criteria' => 'The seeded ticket should be visible in the backlog list.',
|
||||
@@ -44,6 +47,7 @@ class TicketSeeder extends Seeder
|
||||
'description' => 'Seeded backlog item for demonstrating the first ticket workflow.',
|
||||
'story_points' => 3,
|
||||
'ticket_priority_id' => $priority->id,
|
||||
'ticket_status_id' => $defaultStatus->id,
|
||||
'ticket_type_id' => $defaultType->id,
|
||||
'title' => 'Seed backlog item',
|
||||
], $author);
|
||||
@@ -54,6 +58,7 @@ class TicketSeeder extends Seeder
|
||||
'description' => 'Second seeded backlog item using the same title on purpose.',
|
||||
'story_points' => 5,
|
||||
'ticket_priority_id' => $priority->id,
|
||||
'ticket_status_id' => $defaultStatus->id,
|
||||
'ticket_type_id' => $defaultType->id,
|
||||
'title' => 'Seed backlog item',
|
||||
], $author);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
|
||||
use App\Models\BoardColumn;
|
||||
use App\Models\Project;
|
||||
use App\Models\TicketStatus;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class TicketStatusSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$initializeProjectTicketMetadata = app(InitializeProjectTicketMetadata::class);
|
||||
|
||||
Project::query()
|
||||
->with('boardColumns')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->each(function (Project $project) use ($initializeProjectTicketMetadata): void {
|
||||
$initializeProjectTicketMetadata->handle($project);
|
||||
|
||||
TicketStatus::factory()
|
||||
->for($project)
|
||||
->create([
|
||||
'name' => 'Ready',
|
||||
'position' => $project->ticketStatuses()->whereNull('board_column_id')->count() + 1,
|
||||
]);
|
||||
|
||||
$project->boardColumns->each(function (BoardColumn $boardColumn): void {
|
||||
TicketStatus::factory()
|
||||
->for($boardColumn->project)
|
||||
->for($boardColumn)
|
||||
->create([
|
||||
'name' => "{$boardColumn->name} Review",
|
||||
'position' => $boardColumn->ticketStatuses()->count() + 1,
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user