Revert "Merge pull request 'KAN-11 Move remaining project metadata ownership to templates' (#94) from koda/kan-11-restliche-projekt-konfiguration-auf-templates-umstellen into epic/kan-3"
This reverts commitd39d21d303, reversing changes made toa8952d7867.
This commit is contained in:
@@ -27,13 +27,6 @@ class CreateProject
|
||||
{
|
||||
return DB::transaction(function () use ($attributes, $actor): Project {
|
||||
$project = Project::query()->create(Arr::only($attributes, ['key', 'name']));
|
||||
$baseTemplate = $this->createProjectBaseTemplate->handle($project);
|
||||
|
||||
$project->forceFill([
|
||||
'project_template_id' => $baseTemplate->id,
|
||||
])->save();
|
||||
|
||||
$project = $project->fresh() ?? $project;
|
||||
|
||||
$this->initializeProjectTicketMetadata->handle($project);
|
||||
$this->permissionService->grant($actor, Permission::ProjectAdmin, $project);
|
||||
@@ -57,14 +50,19 @@ class CreateProject
|
||||
$project,
|
||||
$template->configuration_snapshot ?? [],
|
||||
);
|
||||
|
||||
$project->forceFill([
|
||||
'project_template_id' => $template->id,
|
||||
])->save();
|
||||
}
|
||||
|
||||
$baseTemplate->forceFill([
|
||||
'configuration_snapshot' => $this->duplicateProjectConfiguration->export(
|
||||
$project->fresh() ?? $project,
|
||||
DuplicateProjectConfiguration::copyableAreas(),
|
||||
),
|
||||
if ($project->project_template_id === null) {
|
||||
$baseTemplate = $this->createProjectBaseTemplate->handle($project->fresh() ?? $project);
|
||||
|
||||
$project->forceFill([
|
||||
'project_template_id' => $baseTemplate->id,
|
||||
])->save();
|
||||
}
|
||||
|
||||
return $project->fresh() ?? $project;
|
||||
});
|
||||
|
||||
@@ -47,13 +47,9 @@ class CreateProjectTicket
|
||||
$nextSequenceNumber = (int) Ticket::query()
|
||||
->where('project_id', $project->id)
|
||||
->max('sequence_number') + 1;
|
||||
$ticketStatusQuery = TicketStatus::query()
|
||||
->where(
|
||||
$project->project_template_id === null ? 'project_id' : 'project_template_id',
|
||||
$project->project_template_id ?? $project->id,
|
||||
);
|
||||
|
||||
$ticketStatus = $ticketStatusQuery->findOrFail(Arr::get($attributes, 'ticket_status_id'));
|
||||
$ticketStatus = TicketStatus::query()
|
||||
->where('project_id', $project->id)
|
||||
->findOrFail(Arr::get($attributes, 'ticket_status_id'));
|
||||
|
||||
$ticket = Ticket::query()->create([
|
||||
'assignee_user_id' => Arr::get($attributes, 'assignee_user_id'),
|
||||
|
||||
@@ -24,17 +24,12 @@ class MoveProjectTicketRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'ticket_status_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_statuses', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_statuses', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -29,17 +29,12 @@ class DestroyProjectTicketTypeRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'replacement_ticket_type_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('ticket_types', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_types', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -29,17 +29,12 @@ class MoveProjectTicketRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'ticket_status_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_statuses', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_statuses', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
'redirect_to' => ['required', 'string', Rule::in(['backlog', 'board'])],
|
||||
];
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Project;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreProjectTicketBranchRequest extends FormRequest
|
||||
{
|
||||
@@ -24,21 +22,9 @@ class StoreProjectTicketBranchRequest extends FormRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'branch_name' => ['required', 'string', 'max:255'],
|
||||
'repository_connection_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('repository_connections', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
'repository_connection_id' => ['required', 'integer', 'exists:repository_connections,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,9 +48,6 @@ class StoreProjectTicketRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'acceptance_criteria' => ['nullable', 'array'],
|
||||
@@ -69,23 +66,17 @@ class StoreProjectTicketRequest extends FormRequest
|
||||
'ticket_priority_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_priorities', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_priorities', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
'ticket_status_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_statuses', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_statuses', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
'ticket_type_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_types', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_types', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
'story_points' => ['nullable', 'integer', 'min:0'],
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
|
||||
@@ -28,9 +28,6 @@ class StoreTicketLinkRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'linked_ticket_id' => [
|
||||
@@ -41,9 +38,7 @@ class StoreTicketLinkRequest extends FormRequest
|
||||
'ticket_link_type_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_link_types', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_link_types', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -26,17 +26,12 @@ class StoreTicketStatusRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'board_column_id' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('board_columns', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('board_columns', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
];
|
||||
|
||||
@@ -51,9 +51,6 @@ class UpdateProjectTicketRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'acceptance_criteria' => ['nullable', 'array'],
|
||||
@@ -71,9 +68,7 @@ class UpdateProjectTicketRequest extends FormRequest
|
||||
'field_values.*.ticket_field_definition_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_field_definitions', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_field_definitions', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
'field_values.*.value_number' => ['nullable', 'numeric'],
|
||||
'field_values.*.value_user_id' => ['nullable', 'integer', Rule::exists('users', 'id')],
|
||||
@@ -86,16 +81,12 @@ class UpdateProjectTicketRequest extends FormRequest
|
||||
'ticket_priority_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_priorities', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_priorities', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
'ticket_type_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('ticket_types', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_types', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'story_points' => ['nullable', 'integer', 'min:0'],
|
||||
@@ -150,7 +141,7 @@ class UpdateProjectTicketRequest extends FormRequest
|
||||
|
||||
$requestedTicketTypeId = Arr::get($validated, 'ticket_type_id');
|
||||
$requestedTicketType = TicketType::query()
|
||||
->where($project->project_template_id === null ? 'project_id' : 'project_template_id', $project->project_template_id ?? $project->id)
|
||||
->where('project_id', $project->id)
|
||||
->find($requestedTicketTypeId);
|
||||
|
||||
if (
|
||||
|
||||
@@ -29,17 +29,12 @@ class UpdateTicketStatusTransitionsRequest extends FormRequest
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'allowed_ticket_status_ids' => ['nullable', 'array'],
|
||||
'allowed_ticket_status_ids.*' => [
|
||||
'integer',
|
||||
Rule::exists('ticket_statuses', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
Rule::exists('ticket_statuses', 'id')->where('project_id', $project->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Project;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ValidateProjectTicketBranchRequest extends FormRequest
|
||||
{
|
||||
@@ -24,21 +22,9 @@ class ValidateProjectTicketBranchRequest extends FormRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
/** @var Project $project */
|
||||
$project = $this->route('project');
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
$ownershipColumn = $projectTemplateId === null ? 'project_id' : 'project_template_id';
|
||||
$ownershipValue = $projectTemplateId ?? $project->id;
|
||||
|
||||
return [
|
||||
'branch_name' => ['required', 'string', 'max:255'],
|
||||
'repository_connection_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('repository_connections', 'id')->where(
|
||||
fn ($query) => $query->where($ownershipColumn, $ownershipValue),
|
||||
),
|
||||
],
|
||||
'repository_connection_id' => ['required', 'integer', 'exists:repository_connections,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\SyncsProjectTemplateOwnership;
|
||||
use Database\Factories\BoardColumnFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -11,19 +10,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
|
||||
#[Fillable(['name', 'position', 'project_id', 'project_template_id'])]
|
||||
#[Fillable(['name', 'position', 'project_id'])]
|
||||
class BoardColumn extends Model
|
||||
{
|
||||
/** @use HasFactory<BoardColumnFactory> */
|
||||
use HasFactory;
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $boardColumn): void {
|
||||
$boardColumn->syncProjectTemplateOwnership();
|
||||
});
|
||||
}
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectTemplate;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
trait SyncsProjectTemplateOwnership
|
||||
{
|
||||
protected function syncProjectTemplateOwnership(): void
|
||||
{
|
||||
$projectTemplateId = $this->getAttribute('project_template_id');
|
||||
$projectId = $this->getAttribute('project_id');
|
||||
|
||||
if ($projectTemplateId === null && $projectId !== null) {
|
||||
$projectTemplateId = Project::query()
|
||||
->whereKey($projectId)
|
||||
->value('project_template_id');
|
||||
|
||||
if ($projectTemplateId !== null) {
|
||||
$this->setAttribute('project_template_id', $projectTemplateId);
|
||||
}
|
||||
}
|
||||
|
||||
if ($projectId === null && $projectTemplateId !== null) {
|
||||
$projectId = ProjectTemplate::query()
|
||||
->whereKey($projectTemplateId)
|
||||
->value('project_id');
|
||||
|
||||
if ($projectId !== null) {
|
||||
$this->setAttribute('project_id', $projectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function projectTemplate(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProjectTemplate::class);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ namespace App\Models;
|
||||
|
||||
use App\Enums\NotificationChannel;
|
||||
use App\Enums\NotificationEventType;
|
||||
use App\Models\Concerns\SyncsProjectTemplateOwnership;
|
||||
use Database\Factories\NotificationActionFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
@@ -24,14 +23,12 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
'message_template',
|
||||
'name',
|
||||
'project_id',
|
||||
'project_template_id',
|
||||
])]
|
||||
#[Hidden(['delivery_url'])]
|
||||
class NotificationAction extends Model
|
||||
{
|
||||
/** @use HasFactory<NotificationActionFactory> */
|
||||
use HasFactory;
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
@@ -48,13 +45,6 @@ class NotificationAction extends Model
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $notificationAction): void {
|
||||
$notificationAction->syncProjectTemplateOwnership();
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeEnabled(Builder $query): Builder
|
||||
{
|
||||
return $query->where('enabled', true);
|
||||
|
||||
+8
-40
@@ -59,83 +59,51 @@ class Project extends Model
|
||||
|
||||
public function boardColumns(): HasMany
|
||||
{
|
||||
return $this->hasMany(
|
||||
BoardColumn::class,
|
||||
$this->project_template_id === null ? 'project_id' : 'project_template_id',
|
||||
$this->project_template_id === null ? 'id' : 'project_template_id',
|
||||
)
|
||||
return $this->hasMany(BoardColumn::class)
|
||||
->orderBy('position');
|
||||
}
|
||||
|
||||
public function ticketFieldDefinitions(): HasMany
|
||||
{
|
||||
return $this->hasMany(
|
||||
TicketFieldDefinition::class,
|
||||
$this->project_template_id === null ? 'project_id' : 'project_template_id',
|
||||
$this->project_template_id === null ? 'id' : 'project_template_id',
|
||||
)
|
||||
return $this->hasMany(TicketFieldDefinition::class)
|
||||
->orderBy('position');
|
||||
}
|
||||
|
||||
public function ticketPriorities(): HasMany
|
||||
{
|
||||
return $this->hasMany(
|
||||
TicketPriority::class,
|
||||
$this->project_template_id === null ? 'project_id' : 'project_template_id',
|
||||
$this->project_template_id === null ? 'id' : 'project_template_id',
|
||||
)
|
||||
return $this->hasMany(TicketPriority::class)
|
||||
->orderBy('position');
|
||||
}
|
||||
|
||||
public function ticketLinkTypes(): HasMany
|
||||
{
|
||||
return $this->hasMany(
|
||||
TicketLinkType::class,
|
||||
$this->project_template_id === null ? 'project_id' : 'project_template_id',
|
||||
$this->project_template_id === null ? 'id' : 'project_template_id',
|
||||
)
|
||||
return $this->hasMany(TicketLinkType::class)
|
||||
->orderBy('position');
|
||||
}
|
||||
|
||||
public function ticketStatuses(): HasMany
|
||||
{
|
||||
return $this->hasMany(
|
||||
TicketStatus::class,
|
||||
$this->project_template_id === null ? 'project_id' : 'project_template_id',
|
||||
$this->project_template_id === null ? 'id' : 'project_template_id',
|
||||
)
|
||||
return $this->hasMany(TicketStatus::class)
|
||||
->orderBy('board_column_id')
|
||||
->orderBy('position');
|
||||
}
|
||||
|
||||
public function ticketTypes(): HasMany
|
||||
{
|
||||
return $this->hasMany(
|
||||
TicketType::class,
|
||||
$this->project_template_id === null ? 'project_id' : 'project_template_id',
|
||||
$this->project_template_id === null ? 'id' : 'project_template_id',
|
||||
)
|
||||
return $this->hasMany(TicketType::class)
|
||||
->orderBy('position');
|
||||
}
|
||||
|
||||
public function repositoryConnections(): HasMany
|
||||
{
|
||||
return $this->hasMany(
|
||||
RepositoryConnection::class,
|
||||
$this->project_template_id === null ? 'project_id' : 'project_template_id',
|
||||
$this->project_template_id === null ? 'id' : 'project_template_id',
|
||||
)
|
||||
return $this->hasMany(RepositoryConnection::class)
|
||||
->orderBy('provider')
|
||||
->orderBy('repository_full_name');
|
||||
}
|
||||
|
||||
public function notificationActions(): HasMany
|
||||
{
|
||||
return $this->hasMany(
|
||||
NotificationAction::class,
|
||||
$this->project_template_id === null ? 'project_id' : 'project_template_id',
|
||||
$this->project_template_id === null ? 'id' : 'project_template_id',
|
||||
)
|
||||
return $this->hasMany(NotificationAction::class)
|
||||
->orderBy('name')
|
||||
->orderBy('id');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Models;
|
||||
|
||||
use App\Actions\Git\Data\GitRepository;
|
||||
use App\Actions\Git\GitProviderKey;
|
||||
use App\Models\Concerns\SyncsProjectTemplateOwnership;
|
||||
use Database\Factories\RepositoryConnectionFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -15,7 +14,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
'base_url',
|
||||
'owner_name',
|
||||
'project_id',
|
||||
'project_template_id',
|
||||
'provider',
|
||||
'provider_repository_id',
|
||||
'repository_full_name',
|
||||
@@ -26,7 +24,6 @@ class RepositoryConnection extends Model
|
||||
{
|
||||
/** @use HasFactory<RepositoryConnectionFactory> */
|
||||
use HasFactory;
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
@@ -38,13 +35,6 @@ class RepositoryConnection extends Model
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $repositoryConnection): void {
|
||||
$repositoryConnection->syncProjectTemplateOwnership();
|
||||
});
|
||||
}
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\SyncsProjectTemplateOwnership;
|
||||
use Database\Factories\TicketFieldDefinitionFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,7 +9,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['field_type', 'label', 'position', 'project_id', 'project_template_id'])]
|
||||
#[Fillable(['field_type', 'label', 'position', 'project_id'])]
|
||||
class TicketFieldDefinition extends Model
|
||||
{
|
||||
/**
|
||||
@@ -23,14 +22,6 @@ class TicketFieldDefinition extends Model
|
||||
|
||||
/** @use HasFactory<TicketFieldDefinitionFactory> */
|
||||
use HasFactory;
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $ticketFieldDefinition): void {
|
||||
$ticketFieldDefinition->syncProjectTemplateOwnership();
|
||||
});
|
||||
}
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\SyncsProjectTemplateOwnership;
|
||||
use Database\Factories\TicketLinkTypeFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,19 +9,11 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['label', 'counterpart_label', 'position', 'project_id', 'project_template_id'])]
|
||||
#[Fillable(['label', 'counterpart_label', 'position', 'project_id'])]
|
||||
class TicketLinkType extends Model
|
||||
{
|
||||
/** @use HasFactory<TicketLinkTypeFactory> */
|
||||
use HasFactory;
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $ticketLinkType): void {
|
||||
$ticketLinkType->syncProjectTemplateOwnership();
|
||||
});
|
||||
}
|
||||
|
||||
public function counterpartLabel(): string
|
||||
{
|
||||
|
||||
@@ -2,26 +2,17 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\SyncsProjectTemplateOwnership;
|
||||
use Database\Factories\TicketPriorityFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable(['name', 'position', 'project_id', 'project_template_id'])]
|
||||
#[Fillable(['name', 'position', 'project_id'])]
|
||||
class TicketPriority extends Model
|
||||
{
|
||||
/** @use HasFactory<TicketPriorityFactory> */
|
||||
use HasFactory;
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $ticketPriority): void {
|
||||
$ticketPriority->syncProjectTemplateOwnership();
|
||||
});
|
||||
}
|
||||
|
||||
public function project(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\SyncsProjectTemplateOwnership;
|
||||
use Database\Factories\TicketStatusFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -12,23 +11,15 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Fillable(['board_column_id', 'name', 'position', 'project_id', 'project_template_id', 'slug'])]
|
||||
#[Fillable(['board_column_id', 'name', 'position', 'project_id', 'slug'])]
|
||||
class TicketStatus extends Model
|
||||
{
|
||||
/** @use HasFactory<TicketStatusFactory> */
|
||||
use HasFactory;
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $ticketStatus): void {
|
||||
if ($ticketStatus->project_template_id === null && $ticketStatus->board_column_id !== null) {
|
||||
$ticketStatus->project_template_id = BoardColumn::query()
|
||||
->whereKey($ticketStatus->board_column_id)
|
||||
->value('project_template_id');
|
||||
}
|
||||
|
||||
$ticketStatus->syncProjectTemplateOwnership();
|
||||
$ticketStatus->slug = Str::slug($ticketStatus->name);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\SyncsProjectTemplateOwnership;
|
||||
use Database\Factories\TicketTypeFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,19 +9,11 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['name', 'position', 'project_id', 'project_template_id', 'is_parent'])]
|
||||
#[Fillable(['name', 'position', 'project_id', 'is_parent'])]
|
||||
class TicketType extends Model
|
||||
{
|
||||
/** @use HasFactory<TicketTypeFactory> */
|
||||
use HasFactory;
|
||||
use SyncsProjectTemplateOwnership;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $ticketType): void {
|
||||
$ticketType->syncProjectTemplateOwnership();
|
||||
});
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||
use App\Models\Project;
|
||||
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
|
||||
{
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private array $tables = [
|
||||
'board_columns',
|
||||
'ticket_field_definitions',
|
||||
'ticket_link_types',
|
||||
'ticket_priorities',
|
||||
'ticket_statuses',
|
||||
'ticket_types',
|
||||
'repository_connections',
|
||||
'notification_actions',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach ($this->tables as $table) {
|
||||
Schema::table($table, function (Blueprint $table): void {
|
||||
$table->foreignId('project_template_id')
|
||||
->nullable()
|
||||
->after('project_id')
|
||||
->constrained('project_templates')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
$projects = DB::table('projects')
|
||||
->leftJoin('project_templates', 'project_templates.id', '=', 'projects.project_template_id')
|
||||
->select([
|
||||
'projects.id',
|
||||
'projects.key',
|
||||
'projects.name',
|
||||
'projects.project_template_id',
|
||||
'project_templates.project_id as template_project_id',
|
||||
])
|
||||
->get();
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$projectTemplateId = $project->project_template_id;
|
||||
|
||||
if ($projectTemplateId === null || (int) ($project->template_project_id ?? 0) !== (int) $project->id) {
|
||||
$projectTemplateId = DB::table('project_templates')->insertGetId([
|
||||
'project_id' => $project->id,
|
||||
'source_project_id' => $project->id,
|
||||
'name' => $project->name.' base template',
|
||||
'slug' => Str::lower($project->key).'-base-template',
|
||||
'is_published' => false,
|
||||
'copy_areas' => json_encode([
|
||||
'project_settings',
|
||||
'board_workflow',
|
||||
'ticket_types',
|
||||
'ticket_priorities',
|
||||
'ticket_fields',
|
||||
'ticket_link_types',
|
||||
'repository_connections',
|
||||
'notification_actions',
|
||||
], JSON_THROW_ON_ERROR),
|
||||
'configuration_snapshot' => json_encode([], JSON_THROW_ON_ERROR),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('projects')
|
||||
->where('id', $project->id)
|
||||
->update([
|
||||
'project_template_id' => $projectTemplateId,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($this->tables as $table) {
|
||||
DB::table($table)
|
||||
->where('project_id', $project->id)
|
||||
->update(['project_template_id' => $projectTemplateId]);
|
||||
}
|
||||
|
||||
$projectModel = Project::query()->find($project->id);
|
||||
|
||||
if ($projectModel !== null) {
|
||||
DB::table('project_templates')
|
||||
->where('id', $projectTemplateId)
|
||||
->update([
|
||||
'configuration_snapshot' => json_encode(
|
||||
app(DuplicateProjectConfiguration::class)->export(
|
||||
$projectModel,
|
||||
DuplicateProjectConfiguration::copyableAreas(),
|
||||
),
|
||||
JSON_THROW_ON_ERROR,
|
||||
),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach (array_reverse($this->tables) as $table) {
|
||||
Schema::table($table, function (Blueprint $table): void {
|
||||
$table->dropConstrainedForeignId('project_template_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -102,12 +102,7 @@ class ProjectApiTest extends TestCase
|
||||
|
||||
$project->refresh();
|
||||
|
||||
$this->assertNotSame($template->id, $project->project_template_id);
|
||||
$this->assertDatabaseHas('project_templates', [
|
||||
'id' => $project->project_template_id,
|
||||
'project_id' => $project->id,
|
||||
'is_published' => false,
|
||||
]);
|
||||
$this->assertSame($template->id, $project->project_template_id);
|
||||
$this->assertFalse($project->show_author);
|
||||
$this->assertFalse($project->show_story_points);
|
||||
$this->assertSame(['Spike'], $project->ticketTypes()->pluck('name')->all());
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Tests\Feature\Database;
|
||||
|
||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
@@ -71,76 +70,4 @@ class ProjectTemplateBaselineMigrationTest extends TestCase
|
||||
$this->assertSame(0, $template->is_published);
|
||||
$this->assertSame('lgcy-base-template', $template->slug);
|
||||
}
|
||||
|
||||
public function test_metadata_migration_moves_existing_configuration_to_the_project_template_and_preserves_snapshot(): void
|
||||
{
|
||||
Artisan::call('migrate:fresh', [
|
||||
'--path' => [
|
||||
'database/migrations/0001_01_01_000000_create_users_table.php',
|
||||
'database/migrations/0001_01_01_000001_create_cache_table.php',
|
||||
'database/migrations/0001_01_01_000002_create_jobs_table.php',
|
||||
'database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php',
|
||||
'database/migrations/2026_03_24_234007_create_projects_table.php',
|
||||
'database/migrations/2026_03_24_234734_create_teams_table.php',
|
||||
'database/migrations/2026_03_24_234735_create_team_user_table.php',
|
||||
'database/migrations/2026_03_25_002100_create_board_columns_table.php',
|
||||
'database/migrations/2026_03_25_003100_create_ticket_types_table.php',
|
||||
'database/migrations/2026_03_25_003101_create_ticket_priorities_table.php',
|
||||
'database/migrations/2026_03_25_003102_create_ticket_field_definitions_table.php',
|
||||
'database/migrations/2026_03_25_004098_create_ticket_statuses_table.php',
|
||||
'database/migrations/2026_03_25_004099_create_ticket_statuses_transitionstable.php',
|
||||
'database/migrations/2026_03_25_004100_create_tickets_table.php',
|
||||
'database/migrations/2026_03_25_065302_create_ticket_worklog_entries_table.php',
|
||||
'database/migrations/2026_03_25_070706_create_ticket_field_values_table.php',
|
||||
'database/migrations/2026_03_25_073715_create_ticket_links_table.php',
|
||||
'database/migrations/2026_03_25_073715_create_ticket_link_types_table.php',
|
||||
'database/migrations/2026_03_26_074522_create_user_git_connections_table.php',
|
||||
'database/migrations/2026_03_26_075849_create_repository_connections_table.php',
|
||||
'database/migrations/2026_04_12_120000_create_permission_assignments_table.php',
|
||||
'database/migrations/2026_04_20_174556_create_user_applications_table.php',
|
||||
'database/migrations/2026_04_20_194646_create_notification_actions_table.php',
|
||||
'database/migrations/2026_04_20_194646_create_notification_delivery_logs_table.php',
|
||||
'database/migrations/2026_04_24_194800_add_filters_to_notification_actions_table.php',
|
||||
'database/migrations/2026_04_24_203500_create_acceptance_criteria_table.php',
|
||||
'database/migrations/2026_05_19_230500_drop_acceptance_criteria_from_tickets_table.php',
|
||||
'database/migrations/2026_05_22_124700_add_must_change_password_on_next_login_to_users_table.php',
|
||||
'database/migrations/2026_05_23_101900_add_counterpart_label_to_ticket_link_types_table.php',
|
||||
'database/migrations/2026_05_25_011500_create_project_templates_table.php',
|
||||
'database/migrations/2026_05_25_013000_project_templates_as_project_baselines.php',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('projects')->insert([
|
||||
'name' => 'Legacy Project',
|
||||
'key' => 'LGCY',
|
||||
'show_author' => 0,
|
||||
'show_story_points' => 1,
|
||||
'project_template_id' => null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$projectId = (int) DB::table('projects')->where('key', 'LGCY')->value('id');
|
||||
|
||||
DB::table('ticket_types')->insert([
|
||||
'project_id' => $projectId,
|
||||
'name' => 'Bug',
|
||||
'position' => 1,
|
||||
'is_parent' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
Artisan::call('migrate', [
|
||||
'--path' => 'database/migrations/2026_06_05_161500_move_project_metadata_to_project_templates.php',
|
||||
]);
|
||||
|
||||
$project = DB::table('projects')->where('id', $projectId)->first();
|
||||
$template = DB::table('project_templates')->where('id', $project->project_template_id)->first();
|
||||
$ticketType = DB::table('ticket_types')->where('project_id', $projectId)->first();
|
||||
$snapshot = json_decode((string) $template->configuration_snapshot, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$this->assertSame($project->project_template_id, $ticketType->project_template_id);
|
||||
$this->assertSame(['Bug'], array_column($snapshot[DuplicateProjectConfiguration::COPY_TICKET_TYPES], 'name'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,12 +285,7 @@ class ProjectControllerTest extends TestCase
|
||||
|
||||
$project->refresh();
|
||||
|
||||
$this->assertNotSame($template->id, $project->project_template_id);
|
||||
$this->assertDatabaseHas('project_templates', [
|
||||
'id' => $project->project_template_id,
|
||||
'project_id' => $project->id,
|
||||
'is_published' => false,
|
||||
]);
|
||||
$this->assertSame($template->id, $project->project_template_id);
|
||||
$this->assertSame(false, $project->show_author);
|
||||
$this->assertSame(false, $project->show_story_points);
|
||||
$this->assertDatabaseHas('ticket_types', [
|
||||
|
||||
Reference in New Issue
Block a user