fix: make project template inheritance granular and safe
linter / quality (pull_request) Successful in 2m52s
tests / ci (8.4) (pull_request) Successful in 3m15s

This commit is contained in:
Nico Schröder
2026-09-08 12:03:07 +02:00
parent 0f125f17a3
commit 3122e00ed8
50 changed files with 1504 additions and 624 deletions
@@ -30,10 +30,16 @@ class ResolveProjectTemplateState
? DuplicateProjectConfiguration::copyableAreas()
: $this->duplicateProjectConfiguration->normalizeCopyAreas($areas);
$templateConfiguration = collect($projectTemplate->configuration_snapshot ?? [])
$normalizedTemplateConfiguration = $this->duplicateProjectConfiguration->normalizeConfiguration(
$projectTemplate->configuration_snapshot ?? [],
);
$templateConfiguration = collect($normalizedTemplateConfiguration)
->only($selectedAreas)
->all();
$currentConfiguration = $this->duplicateProjectConfiguration->export($project, $selectedAreas);
$persistedOverrides = $projectTemplate->is_published
? array_values(array_intersect($project->template_overrides ?? [], $selectedAreas))
: [];
$overriddenAreas = [];
$inheritedAreas = [];
@@ -41,13 +47,12 @@ class ResolveProjectTemplateState
foreach ($selectedAreas as $area) {
$label = $this->labelForArea($area);
$hasTemplateValue = array_key_exists($area, $templateConfiguration);
$hasCurrentValue = array_key_exists($area, $currentConfiguration);
if (! $hasTemplateValue && ! $hasCurrentValue) {
if (! $hasTemplateValue) {
continue;
}
if ($hasTemplateValue && $hasCurrentValue && $currentConfiguration[$area] === $templateConfiguration[$area]) {
if (! in_array($area, $persistedOverrides, true)) {
$inheritedAreas[] = [
'label' => $label,
'value' => $area,
@@ -72,6 +77,7 @@ class ResolveProjectTemplateState
'inheritsFromTemplate' => $projectTemplate->is_published,
'overriddenAreas' => $overriddenAreas,
'overriddenConfiguration' => array_intersect_key($currentConfiguration, $overriddenAreaValues),
'resetRequirements' => $this->resetRequirements($project, $templateConfiguration, $persistedOverrides),
'templateConfiguration' => $templateConfiguration,
'templateId' => $projectTemplate->id,
'templateName' => $projectTemplate->name,
@@ -79,6 +85,44 @@ class ResolveProjectTemplateState
];
}
/**
* @param array<string, mixed> $templateConfiguration
* @param list<string> $persistedOverrides
* @return array<string, mixed>
*/
private function resetRequirements(Project $project, array $templateConfiguration, array $persistedOverrides): array
{
if (! in_array(DuplicateProjectConfiguration::COPY_TICKET_STATUSES, $persistedOverrides, true)) {
return [];
}
$templateStatuses = $templateConfiguration[DuplicateProjectConfiguration::COPY_TICKET_STATUSES] ?? [];
if (! is_array($templateStatuses)) {
return [];
}
$statuses = $project->ticketStatuses()->withCount('tickets')->get()->values();
$replacementOptions = $statuses->take(count($templateStatuses))->map(
static fn ($status): array => ['id' => $status->id, 'name' => $status->name],
)->all();
$removedStatuses = $statuses->slice(count($templateStatuses))
->filter(static fn ($status): bool => $status->tickets_count > 0)
->map(static fn ($status): array => [
'id' => $status->id,
'name' => $status->name,
'ticketCount' => $status->tickets_count,
])
->values()
->all();
return $removedStatuses === [] ? [] : [
DuplicateProjectConfiguration::COPY_TICKET_STATUSES => [
'removedStatuses' => $removedStatuses,
'replacementOptions' => $replacementOptions,
],
];
}
private function labelForArea(string $area): string
{
foreach (DuplicateProjectConfiguration::copyAreaOptions() as $option) {