Show template inheritance overrides

This commit is contained in:
2026-09-08 10:25:52 +00:00
committed by Twyco
parent fef82cb88f
commit 831014f7fe
16 changed files with 334 additions and 4 deletions
@@ -2,6 +2,8 @@
namespace App\Actions\Notifications;
use App\Actions\Projects\DuplicateProjectConfiguration;
use App\Actions\Projects\ResolveProjectTemplateState;
use App\Enums\DiscordAllowedMentionType;
use App\Enums\NotificationChannel;
use App\Enums\NotificationEventType;
@@ -15,6 +17,7 @@ class BuildProjectNotificationSettingsPayload
{
public function __construct(
private readonly ListProjectNotificationActions $listProjectNotificationActions,
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
) {}
/**
@@ -33,6 +36,9 @@ class BuildProjectNotificationSettingsPayload
)->resolve(),
'project' => ProjectResource::make($project)->resolve(),
'status' => $status,
'templateState' => $this->resolveProjectTemplateState->handle($project, [
DuplicateProjectConfiguration::COPY_NOTIFICATION_ACTIONS,
]),
'templateVariables' => $this->templateVariables(),
];
}
@@ -9,6 +9,10 @@ use App\Models\Project;
class BuildProjectBoardSettingsPayload
{
public function __construct(
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
) {}
/**
* @return array<string, mixed>
*/
@@ -27,6 +31,9 @@ class BuildProjectBoardSettingsPayload
)->resolve(),
'project' => ProjectResource::make($project)->resolve(),
'status' => $status,
'templateState' => $this->resolveProjectTemplateState->handle($project, [
DuplicateProjectConfiguration::COPY_BOARD_WORKFLOW,
]),
'ticketStatuses' => TicketStatusResource::collection(
$project->ticketStatuses()
->with(['allowedTargetStatuses.boardColumn', 'boardColumn'])
@@ -13,6 +13,10 @@ use App\Models\TicketFieldDefinition;
class BuildProjectTicketMetadataSettingsPayload
{
public function __construct(
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
) {}
/**
* @return array<string, mixed>
*/
@@ -23,6 +27,13 @@ class BuildProjectTicketMetadataSettingsPayload
'project' => ProjectResource::make($project)->resolve(),
'error' => $error,
'status' => $status,
'templateState' => $this->resolveProjectTemplateState->handle($project, [
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
DuplicateProjectConfiguration::COPY_TICKET_TYPES,
DuplicateProjectConfiguration::COPY_TICKET_PRIORITIES,
DuplicateProjectConfiguration::COPY_TICKET_FIELDS,
DuplicateProjectConfiguration::COPY_TICKET_LINK_TYPES,
]),
'ticketFieldDefinitions' => TicketFieldDefinitionResource::collection($project->ticketFieldDefinitions()->get())->resolve(),
'ticketLinkTypes' => TicketLinkTypeResource::collection($project->ticketLinkTypes()->get())->resolve(),
'ticketPriorities' => TicketPriorityResource::collection($project->ticketPriorities()->get())->resolve(),
@@ -0,0 +1,86 @@
<?php
namespace App\Actions\Projects;
use App\Models\Project;
use App\Models\ProjectTemplate;
class ResolveProjectTemplateState
{
public function __construct(
private readonly DuplicateProjectConfiguration $duplicateProjectConfiguration,
) {}
/**
* @param list<string>|null $areas
* @return array<string, mixed>|null
*/
public function handle(Project $project, ?array $areas = null): ?array
{
$project->loadMissing('projectTemplate');
/** @var ProjectTemplate|null $projectTemplate */
$projectTemplate = $project->projectTemplate;
if ($projectTemplate === null) {
return null;
}
$selectedAreas = $areas === null
? DuplicateProjectConfiguration::copyableAreas()
: $this->duplicateProjectConfiguration->normalizeCopyAreas($areas);
$templateConfiguration = collect($projectTemplate->configuration_snapshot ?? [])
->only($selectedAreas)
->all();
$currentConfiguration = $this->duplicateProjectConfiguration->export($project, $selectedAreas);
$overriddenAreas = [];
$inheritedAreas = [];
foreach ($selectedAreas as $area) {
$label = $this->labelForArea($area);
$hasTemplateValue = array_key_exists($area, $templateConfiguration);
$hasCurrentValue = array_key_exists($area, $currentConfiguration);
if (! $hasTemplateValue && ! $hasCurrentValue) {
continue;
}
if ($hasTemplateValue && $hasCurrentValue && $currentConfiguration[$area] === $templateConfiguration[$area]) {
$inheritedAreas[] = [
'label' => $label,
'value' => $area,
];
continue;
}
$overriddenAreas[] = [
'label' => $label,
'value' => $area,
];
}
return [
'effectiveConfiguration' => array_replace($templateConfiguration, array_intersect_key($currentConfiguration, array_flip(array_column($overriddenAreas, 'value')))),
'inheritedAreas' => $inheritedAreas,
'inheritsFromTemplate' => true,
'overriddenAreas' => $overriddenAreas,
'templateId' => $projectTemplate->id,
'templateName' => $projectTemplate->name,
'templateSlug' => $projectTemplate->slug,
];
}
private function labelForArea(string $area): string
{
foreach (DuplicateProjectConfiguration::copyAreaOptions() as $option) {
if ($option['value'] === $area) {
return $option['label'];
}
}
return $area;
}
}
@@ -4,6 +4,8 @@ namespace App\Actions\Repositories;
use App\Actions\Git\Data\GitRepository;
use App\Actions\Git\GitProviderManager;
use App\Actions\Projects\DuplicateProjectConfiguration;
use App\Actions\Projects\ResolveProjectTemplateState;
use App\Http\Resources\AvailableGitRepositorySourceResource;
use App\Http\Resources\ProjectResource;
use App\Http\Resources\RepositoryConnectionResource;
@@ -18,6 +20,7 @@ class BuildProjectRepositorySettingsPayload
{
public function __construct(
private readonly GitProviderManager $gitProviderManager,
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
) {}
/**
@@ -41,6 +44,9 @@ class BuildProjectRepositorySettingsPayload
'project' => ProjectResource::make($project)->resolve(),
'repositoryConnections' => RepositoryConnectionResource::collection($repositoryConnections)->resolve(),
'status' => $status,
'templateState' => $this->resolveProjectTemplateState->handle($project, [
DuplicateProjectConfiguration::COPY_REPOSITORY_CONNECTIONS,
]),
];
}
@@ -8,6 +8,7 @@ use App\Actions\Projects\CreateProject;
use App\Actions\Projects\DeleteProject;
use App\Actions\Projects\DuplicateProjectConfiguration;
use App\Actions\Projects\ListVisibleProjects;
use App\Actions\Projects\ResolveProjectTemplateState;
use App\Enums\Permission;
use App\Http\Requests\DeleteProjectRequest;
use App\Http\Requests\StoreProjectRequest;
@@ -35,6 +36,7 @@ class ProjectController extends Controller
private readonly DeleteProject $deleteProject,
private readonly ListVisibleProjects $listVisibleProjects,
private readonly PermissionService $permissionService,
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
) {}
public function index(Request $request): Response
@@ -92,6 +94,7 @@ class ProjectController extends Controller
return Inertia::render('projects/Show', [
'canManageAccess' => $this->permissionService->allows($user, Permission::ProjectAdmin, $project),
'project' => ProjectResource::make($project)->resolve(),
'templateState' => $this->resolveProjectTemplateState->handle($project),
]);
}