Merge pull request 'koda/kan-10-edit-templates' (#102) from koda/kan-10-edit-templates into epic/kan-3
Reviewed-on: OpenSource/KanbanGit#102
This commit was merged in pull request #102.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Projects;
|
||||
|
||||
use App\Models\ProjectTemplate;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateProjectTemplate
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DuplicateProjectConfiguration $duplicateProjectConfiguration,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array{name: string, copy_areas?: array<int, string>} $attributes
|
||||
*/
|
||||
public function handle(ProjectTemplate $projectTemplate, array $attributes): ProjectTemplate
|
||||
{
|
||||
$projectTemplate->loadMissing('sourceProject', 'linkedProjects.projectTemplate');
|
||||
|
||||
$sourceProject = $projectTemplate->sourceProject;
|
||||
abort_if($sourceProject === null, 422, 'This template cannot be edited because the source project is missing.');
|
||||
|
||||
$copyAreas = array_values(Arr::get($attributes, 'copy_areas', []));
|
||||
$normalizedCopyAreas = $this->duplicateProjectConfiguration->normalizeCopyAreas($copyAreas);
|
||||
$previousConfiguration = $projectTemplate->configuration_snapshot ?? [];
|
||||
$nextConfiguration = $this->duplicateProjectConfiguration->export($sourceProject, $normalizedCopyAreas);
|
||||
$affectedAreas = array_values(array_unique([
|
||||
...array_keys($previousConfiguration),
|
||||
...array_keys($nextConfiguration),
|
||||
]));
|
||||
|
||||
DB::transaction(function () use ($projectTemplate, $attributes, $normalizedCopyAreas, $nextConfiguration, $previousConfiguration, $affectedAreas): void {
|
||||
$projectTemplate->forceFill([
|
||||
'name' => $attributes['name'],
|
||||
'copy_areas' => $normalizedCopyAreas,
|
||||
'configuration_snapshot' => $nextConfiguration,
|
||||
])->save();
|
||||
|
||||
foreach ($projectTemplate->linkedProjects as $linkedProject) {
|
||||
$currentConfiguration = $this->duplicateProjectConfiguration->export($linkedProject->fresh(), $affectedAreas);
|
||||
$replacementConfiguration = [];
|
||||
|
||||
foreach ($nextConfiguration as $area => $configuration) {
|
||||
$isNewTemplateArea = ! array_key_exists($area, $previousConfiguration);
|
||||
$matchesPreviousTemplate = array_key_exists($area, $previousConfiguration)
|
||||
&& array_key_exists($area, $currentConfiguration)
|
||||
&& $currentConfiguration[$area] === $previousConfiguration[$area];
|
||||
|
||||
if ($isNewTemplateArea || $matchesPreviousTemplate) {
|
||||
$replacementConfiguration[$area] = $configuration;
|
||||
}
|
||||
}
|
||||
|
||||
if ($replacementConfiguration === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->duplicateProjectConfiguration->apply($linkedProject->fresh(), $replacementConfiguration);
|
||||
}
|
||||
});
|
||||
|
||||
return $projectTemplate->fresh(['sourceProject'])->loadCount('linkedProjects');
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Projects\CreateProjectTemplate;
|
||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||
use App\Actions\Projects\UpdateProjectTemplate;
|
||||
use App\Http\Requests\StoreProjectTemplateRequest;
|
||||
use App\Http\Requests\UpdateProjectTemplateRequest;
|
||||
use App\Http\Resources\ProjectResource;
|
||||
use App\Http\Resources\ProjectTemplateResource;
|
||||
use App\Models\Project;
|
||||
@@ -18,6 +20,7 @@ class ProjectTemplateController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CreateProjectTemplate $createProjectTemplate,
|
||||
private readonly UpdateProjectTemplate $updateProjectTemplate,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
@@ -28,12 +31,25 @@ class ProjectTemplateController extends Controller
|
||||
ProjectTemplate::query()
|
||||
->published()
|
||||
->with('sourceProject')
|
||||
->withCount('linkedProjects')
|
||||
->orderBy('name')
|
||||
->get(),
|
||||
)->resolve(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(ProjectTemplate $projectTemplate): Response
|
||||
{
|
||||
abort_if(! $projectTemplate->is_published, 404);
|
||||
|
||||
$projectTemplate->load('sourceProject')->loadCount('linkedProjects');
|
||||
|
||||
return Inertia::render('project-templates/Edit', [
|
||||
'copyAreaOptions' => DuplicateProjectConfiguration::copyAreaOptions(),
|
||||
'template' => ProjectTemplateResource::make($projectTemplate)->resolve(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Project $project): Response
|
||||
{
|
||||
return Inertia::render('project-templates/Create', [
|
||||
@@ -50,6 +66,16 @@ class ProjectTemplateController extends Controller
|
||||
->with('status', "Template {$template->name} was created.");
|
||||
}
|
||||
|
||||
public function update(UpdateProjectTemplateRequest $request, ProjectTemplate $projectTemplate): RedirectResponse
|
||||
{
|
||||
abort_if(! $projectTemplate->is_published, 404);
|
||||
|
||||
$template = $this->updateProjectTemplate->handle($projectTemplate, $request->validated());
|
||||
|
||||
return to_route('project-templates.index')
|
||||
->with('status', "Template {$template->name} was updated.");
|
||||
}
|
||||
|
||||
public function destroy(ProjectTemplate $projectTemplate): RedirectResponse
|
||||
{
|
||||
abort_if(! $projectTemplate->is_published, 404);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||
use App\Models\ProjectTemplate;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateProjectTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('create-project') ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
/** @var ProjectTemplate|null $projectTemplate */
|
||||
$projectTemplate = $this->route('projectTemplate');
|
||||
$requiresImpactConfirmation = $projectTemplate?->linkedProjects()->exists() ?? false;
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'copy_areas' => ['required', 'array', 'min:1'],
|
||||
'copy_areas.*' => ['string', Rule::in(DuplicateProjectConfiguration::copyableAreas())],
|
||||
'confirm_linked_projects_update' => [
|
||||
Rule::requiredIf($requiresImpactConfirmation),
|
||||
'accepted',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'confirm_linked_projects_update.accepted' => 'Acknowledge the linked project warning before saving template changes.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ class ProjectTemplateResource extends JsonResource
|
||||
'is_published' => $this->is_published,
|
||||
'copy_areas' => $this->copy_areas ?? [],
|
||||
'configuration_snapshot' => $this->configuration_snapshot ?? [],
|
||||
'linked_project_count' => $this->whenCounted('linkedProjects'),
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
];
|
||||
@@ -44,6 +45,7 @@ class ProjectTemplateResource extends JsonResource
|
||||
'is_published' => Arr::get($this->resource, 'is_published', true),
|
||||
'copy_areas' => Arr::get($this->resource, 'copy_areas', []),
|
||||
'configuration_snapshot' => Arr::get($this->resource, 'configuration_snapshot', []),
|
||||
'linked_project_count' => Arr::get($this->resource, 'linked_project_count'),
|
||||
'created_at' => Arr::get($this->resource, 'created_at'),
|
||||
'updated_at' => Arr::get($this->resource, 'updated_at'),
|
||||
];
|
||||
|
||||
@@ -4,10 +4,11 @@ namespace App\Models;
|
||||
|
||||
use Database\Factories\ProjectTemplateFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['project_id', 'source_project_id', 'name', 'slug', 'is_published', 'copy_areas', 'configuration_snapshot'])]
|
||||
class ProjectTemplate extends Model
|
||||
@@ -38,4 +39,9 @@ class ProjectTemplate extends Model
|
||||
{
|
||||
return $this->belongsTo(Project::class, 'source_project_id');
|
||||
}
|
||||
|
||||
public function linkedProjects(): HasMany
|
||||
{
|
||||
return $this->hasMany(Project::class, 'project_template_id');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user