WIP: Redesign app layout chrome #105
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Actions\Notifications;
|
namespace App\Actions\Notifications;
|
||||||
|
|
||||||
|
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||||
|
use App\Actions\Projects\ResolveProjectTemplateState;
|
||||||
use App\Enums\DiscordAllowedMentionType;
|
use App\Enums\DiscordAllowedMentionType;
|
||||||
use App\Enums\NotificationChannel;
|
use App\Enums\NotificationChannel;
|
||||||
use App\Enums\NotificationEventType;
|
use App\Enums\NotificationEventType;
|
||||||
@@ -15,6 +17,7 @@ class BuildProjectNotificationSettingsPayload
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ListProjectNotificationActions $listProjectNotificationActions,
|
private readonly ListProjectNotificationActions $listProjectNotificationActions,
|
||||||
|
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,6 +36,9 @@ class BuildProjectNotificationSettingsPayload
|
|||||||
)->resolve(),
|
)->resolve(),
|
||||||
'project' => ProjectResource::make($project)->resolve(),
|
'project' => ProjectResource::make($project)->resolve(),
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
|
'templateState' => $this->resolveProjectTemplateState->handle($project, [
|
||||||
|
DuplicateProjectConfiguration::COPY_NOTIFICATION_ACTIONS,
|
||||||
|
]),
|
||||||
'templateVariables' => $this->templateVariables(),
|
'templateVariables' => $this->templateVariables(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ use App\Models\Project;
|
|||||||
|
|
||||||
class BuildProjectBoardSettingsPayload
|
class BuildProjectBoardSettingsPayload
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
@@ -27,6 +31,9 @@ class BuildProjectBoardSettingsPayload
|
|||||||
)->resolve(),
|
)->resolve(),
|
||||||
'project' => ProjectResource::make($project)->resolve(),
|
'project' => ProjectResource::make($project)->resolve(),
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
|
'templateState' => $this->resolveProjectTemplateState->handle($project, [
|
||||||
|
DuplicateProjectConfiguration::COPY_BOARD_WORKFLOW,
|
||||||
|
]),
|
||||||
'ticketStatuses' => TicketStatusResource::collection(
|
'ticketStatuses' => TicketStatusResource::collection(
|
||||||
$project->ticketStatuses()
|
$project->ticketStatuses()
|
||||||
->with(['allowedTargetStatuses.boardColumn', 'boardColumn'])
|
->with(['allowedTargetStatuses.boardColumn', 'boardColumn'])
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ use App\Models\TicketFieldDefinition;
|
|||||||
|
|
||||||
class BuildProjectTicketMetadataSettingsPayload
|
class BuildProjectTicketMetadataSettingsPayload
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
@@ -23,6 +27,13 @@ class BuildProjectTicketMetadataSettingsPayload
|
|||||||
'project' => ProjectResource::make($project)->resolve(),
|
'project' => ProjectResource::make($project)->resolve(),
|
||||||
'error' => $error,
|
'error' => $error,
|
||||||
'status' => $status,
|
'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(),
|
'ticketFieldDefinitions' => TicketFieldDefinitionResource::collection($project->ticketFieldDefinitions()->get())->resolve(),
|
||||||
'ticketLinkTypes' => TicketLinkTypeResource::collection($project->ticketLinkTypes()->get())->resolve(),
|
'ticketLinkTypes' => TicketLinkTypeResource::collection($project->ticketLinkTypes()->get())->resolve(),
|
||||||
'ticketPriorities' => TicketPriorityResource::collection($project->ticketPriorities()->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\Data\GitRepository;
|
||||||
use App\Actions\Git\GitProviderManager;
|
use App\Actions\Git\GitProviderManager;
|
||||||
|
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||||
|
use App\Actions\Projects\ResolveProjectTemplateState;
|
||||||
use App\Http\Resources\AvailableGitRepositorySourceResource;
|
use App\Http\Resources\AvailableGitRepositorySourceResource;
|
||||||
use App\Http\Resources\ProjectResource;
|
use App\Http\Resources\ProjectResource;
|
||||||
use App\Http\Resources\RepositoryConnectionResource;
|
use App\Http\Resources\RepositoryConnectionResource;
|
||||||
@@ -18,6 +20,7 @@ class BuildProjectRepositorySettingsPayload
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly GitProviderManager $gitProviderManager,
|
private readonly GitProviderManager $gitProviderManager,
|
||||||
|
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,6 +44,9 @@ class BuildProjectRepositorySettingsPayload
|
|||||||
'project' => ProjectResource::make($project)->resolve(),
|
'project' => ProjectResource::make($project)->resolve(),
|
||||||
'repositoryConnections' => RepositoryConnectionResource::collection($repositoryConnections)->resolve(),
|
'repositoryConnections' => RepositoryConnectionResource::collection($repositoryConnections)->resolve(),
|
||||||
'status' => $status,
|
'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\DeleteProject;
|
||||||
use App\Actions\Projects\DuplicateProjectConfiguration;
|
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||||
use App\Actions\Projects\ListVisibleProjects;
|
use App\Actions\Projects\ListVisibleProjects;
|
||||||
|
use App\Actions\Projects\ResolveProjectTemplateState;
|
||||||
use App\Enums\Permission;
|
use App\Enums\Permission;
|
||||||
use App\Http\Requests\DeleteProjectRequest;
|
use App\Http\Requests\DeleteProjectRequest;
|
||||||
use App\Http\Requests\StoreProjectRequest;
|
use App\Http\Requests\StoreProjectRequest;
|
||||||
@@ -35,6 +36,7 @@ class ProjectController extends Controller
|
|||||||
private readonly DeleteProject $deleteProject,
|
private readonly DeleteProject $deleteProject,
|
||||||
private readonly ListVisibleProjects $listVisibleProjects,
|
private readonly ListVisibleProjects $listVisibleProjects,
|
||||||
private readonly PermissionService $permissionService,
|
private readonly PermissionService $permissionService,
|
||||||
|
private readonly ResolveProjectTemplateState $resolveProjectTemplateState,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
@@ -92,6 +94,7 @@ class ProjectController extends Controller
|
|||||||
return Inertia::render('projects/Show', [
|
return Inertia::render('projects/Show', [
|
||||||
'canManageAccess' => $this->permissionService->allows($user, Permission::ProjectAdmin, $project),
|
'canManageAccess' => $this->permissionService->allows($user, Permission::ProjectAdmin, $project),
|
||||||
'project' => ProjectResource::make($project)->resolve(),
|
'project' => ProjectResource::make($project)->resolve(),
|
||||||
|
'templateState' => $this->resolveProjectTemplateState->handle($project),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,32 @@ const projectAreas = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Card v-if="templateState?.inheritsFromTemplate">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
Built from template {{ templateState.templateName }}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
This project keeps its template link. Inherited areas follow the template baseline; overridden areas were customized locally.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-wrap gap-2">
|
||||||
|
<Badge
|
||||||
|
v-for="area in templateState.inheritedAreas"
|
||||||
|
:key="`inherited-${area.value}`"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{{ area.label }} · inherited
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-for="area in templateState.overriddenAreas"
|
||||||
|
:key="`overridden-${area.value}`"
|
||||||
|
>
|
||||||
|
{{ area.label }} · overridden
|
||||||
|
</Badge>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div v-if="canManageAccess" class="flex flex-wrap gap-3">
|
<div v-if="canManageAccess" class="flex flex-wrap gap-3">
|
||||||
<Button as-child>
|
<Button as-child>
|
||||||
<Link :href="boardSettings(project.id)">
|
<Link :href="boardSettings(project.id)">
|
||||||
|
|||||||
@@ -138,6 +138,32 @@ const statusAllowsTarget = (
|
|||||||
{{ status }}
|
{{ status }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Card v-if="templateState?.inheritsFromTemplate">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
Template inheritance: {{ templateState.templateName }}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
This project stays linked to its template. Sections below show whether the current configuration is still inherited or has local overrides.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-wrap gap-2">
|
||||||
|
<Badge
|
||||||
|
v-for="area in templateState.inheritedAreas"
|
||||||
|
:key="`inherited-${area.value}`"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{{ area.label }} · inherited
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-for="area in templateState.overriddenAreas"
|
||||||
|
:key="`overridden-${area.value}`"
|
||||||
|
>
|
||||||
|
{{ area.label }} · overridden
|
||||||
|
</Badge>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Workflow model</CardTitle>
|
<CardTitle>Workflow model</CardTitle>
|
||||||
|
|||||||
@@ -115,6 +115,32 @@ const replacementTicketTypes = (ticketTypeId: number) =>
|
|||||||
{{ status }}
|
{{ status }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Card v-if="templateState?.inheritsFromTemplate">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
Template inheritance: {{ templateState.templateName }}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Inherited sections stay synced with the template baseline. Local edits in these settings are treated as project-specific overrides.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-wrap gap-2">
|
||||||
|
<Badge
|
||||||
|
v-for="area in templateState.inheritedAreas"
|
||||||
|
:key="`inherited-${area.value}`"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{{ area.label }} · inherited
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-for="area in templateState.overriddenAreas"
|
||||||
|
:key="`overridden-${area.value}`"
|
||||||
|
>
|
||||||
|
{{ area.label }} · overridden
|
||||||
|
</Badge>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div class="grid gap-6 xl:grid-cols-2">
|
<div class="grid gap-6 xl:grid-cols-2">
|
||||||
<Card class="gap-0 overflow-hidden">
|
<Card class="gap-0 overflow-hidden">
|
||||||
<CardHeader class="border-b">
|
<CardHeader class="border-b">
|
||||||
|
|||||||
@@ -305,6 +305,32 @@ const notificationActionTimestamp = (
|
|||||||
{{ status }}
|
{{ status }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Card v-if="templateState?.inheritsFromTemplate">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
Template inheritance: {{ templateState.templateName }}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Notification rules can remain inherited from the template or diverge here as project-level overrides.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-wrap gap-2">
|
||||||
|
<Badge
|
||||||
|
v-for="area in templateState.inheritedAreas"
|
||||||
|
:key="`inherited-${area.value}`"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{{ area.label }} · inherited
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-for="area in templateState.overriddenAreas"
|
||||||
|
:key="`overridden-${area.value}`"
|
||||||
|
>
|
||||||
|
{{ area.label }} · overridden
|
||||||
|
</Badge>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div class="grid gap-6 xl:grid-cols-[24rem_minmax(0,1fr)]">
|
<div class="grid gap-6 xl:grid-cols-[24rem_minmax(0,1fr)]">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -277,6 +277,32 @@ const submitUpdate = (): void => {
|
|||||||
{{ status }}
|
{{ status }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Card v-if="templateState?.inheritsFromTemplate">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
Template inheritance: {{ templateState.templateName }}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Repository links can stay inherited from the template or be replaced with project-specific overrides.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-wrap gap-2">
|
||||||
|
<Badge
|
||||||
|
v-for="area in templateState.inheritedAreas"
|
||||||
|
:key="`inherited-${area.value}`"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{{ area.label }} · inherited
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-for="area in templateState.overriddenAreas"
|
||||||
|
:key="`overridden-${area.value}`"
|
||||||
|
>
|
||||||
|
{{ area.label }} · overridden
|
||||||
|
</Badge>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Personal Git access</CardTitle>
|
<CardTitle>Personal Git access</CardTitle>
|
||||||
|
|||||||
@@ -61,9 +61,25 @@ export type ProjectTemplatesCreatePageProps = {
|
|||||||
project: ProjectModel;
|
project: ProjectModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProjectTemplateStateArea = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProjectTemplateState = {
|
||||||
|
effectiveConfiguration: Record<string, unknown>;
|
||||||
|
inheritedAreas: ProjectTemplateStateArea[];
|
||||||
|
inheritsFromTemplate: boolean;
|
||||||
|
overriddenAreas: ProjectTemplateStateArea[];
|
||||||
|
templateId: number;
|
||||||
|
templateName: string;
|
||||||
|
templateSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProjectShowPageProps = {
|
export type ProjectShowPageProps = {
|
||||||
canManageAccess: boolean;
|
canManageAccess: boolean;
|
||||||
project: ProjectModel;
|
project: ProjectModel;
|
||||||
|
templateState?: ProjectTemplateState | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PermissionOption = {
|
export type PermissionOption = {
|
||||||
@@ -95,6 +111,7 @@ export type ProjectBoardSettingsPageProps = {
|
|||||||
boardColumns: BoardColumnModel[];
|
boardColumns: BoardColumnModel[];
|
||||||
project: ProjectModel;
|
project: ProjectModel;
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
|
templateState?: ProjectTemplateState | null;
|
||||||
ticketStatuses: TicketStatusModel[];
|
ticketStatuses: TicketStatusModel[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,6 +138,7 @@ export type ProjectTicketMetadataSettingsPageProps = {
|
|||||||
project: ProjectModel;
|
project: ProjectModel;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
|
templateState?: ProjectTemplateState | null;
|
||||||
ticketFieldDefinitions: TicketFieldDefinitionModel[];
|
ticketFieldDefinitions: TicketFieldDefinitionModel[];
|
||||||
ticketLinkTypes: TicketLinkTypeModel[];
|
ticketLinkTypes: TicketLinkTypeModel[];
|
||||||
ticketPriorities: TicketPriorityModel[];
|
ticketPriorities: TicketPriorityModel[];
|
||||||
@@ -132,6 +150,7 @@ export type ProjectRepositorySettingsPageProps = {
|
|||||||
project: ProjectModel;
|
project: ProjectModel;
|
||||||
repositoryConnections: RepositoryConnectionModel[];
|
repositoryConnections: RepositoryConnectionModel[];
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
|
templateState?: ProjectTemplateState | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectNotificationSettingsPageProps = {
|
export type ProjectNotificationSettingsPageProps = {
|
||||||
@@ -143,6 +162,7 @@ export type ProjectNotificationSettingsPageProps = {
|
|||||||
notificationActions: NotificationActionModel[];
|
notificationActions: NotificationActionModel[];
|
||||||
project: ProjectModel;
|
project: ProjectModel;
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
|
templateState?: ProjectTemplateState | null;
|
||||||
templateVariables: NotificationActionTemplateVariableModel[];
|
templateVariables: NotificationActionTemplateVariableModel[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
namespace Tests\Feature\Projects;
|
namespace Tests\Feature\Projects;
|
||||||
|
|
||||||
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
|
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
|
||||||
|
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||||
use App\Models\BoardColumn;
|
use App\Models\BoardColumn;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectTemplate;
|
||||||
use App\Models\Ticket;
|
use App\Models\Ticket;
|
||||||
use App\Models\TicketStatus;
|
use App\Models\TicketStatus;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@@ -31,6 +33,15 @@ class ProjectBoardSettingsControllerTest extends TestCase
|
|||||||
'position' => 1,
|
'position' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$template = ProjectTemplate::factory()->create([
|
||||||
|
'configuration_snapshot' => app(DuplicateProjectConfiguration::class)->export($project, [
|
||||||
|
DuplicateProjectConfiguration::COPY_BOARD_WORKFLOW,
|
||||||
|
]),
|
||||||
|
'copy_areas' => [DuplicateProjectConfiguration::COPY_BOARD_WORKFLOW],
|
||||||
|
'name' => 'Starter Workflow',
|
||||||
|
]);
|
||||||
|
$project->update(['project_template_id' => $template->id]);
|
||||||
|
|
||||||
$this->syncProjectPermissions($project, $user, ['edit']);
|
$this->syncProjectPermissions($project, $user, ['edit']);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
@@ -41,7 +52,9 @@ class ProjectBoardSettingsControllerTest extends TestCase
|
|||||||
->where('project.id', $project->id)
|
->where('project.id', $project->id)
|
||||||
->where('backlogTicketStatuses.0.name', 'Backlog')
|
->where('backlogTicketStatuses.0.name', 'Backlog')
|
||||||
->where('boardColumns.0.name', 'Todo')
|
->where('boardColumns.0.name', 'Todo')
|
||||||
->where('ticketStatuses.0.name', 'Backlog'),
|
->where('ticketStatuses.0.name', 'Backlog')
|
||||||
|
->where('templateState.templateName', 'Starter Workflow')
|
||||||
|
->where('templateState.inheritedAreas.0.value', DuplicateProjectConfiguration::COPY_BOARD_WORKFLOW),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Projects;
|
namespace Tests\Feature\Projects;
|
||||||
|
|
||||||
|
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||||
use App\Enums\NotificationChannel;
|
use App\Enums\NotificationChannel;
|
||||||
use App\Enums\NotificationEventType;
|
use App\Enums\NotificationEventType;
|
||||||
use App\Models\NotificationAction;
|
use App\Models\NotificationAction;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectTemplate;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Inertia\Testing\AssertableInertia as Assert;
|
use Inertia\Testing\AssertableInertia as Assert;
|
||||||
@@ -41,6 +43,15 @@ class ProjectNotificationSettingsControllerTest extends TestCase
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$template = ProjectTemplate::factory()->create([
|
||||||
|
'configuration_snapshot' => app(DuplicateProjectConfiguration::class)->export($project, [
|
||||||
|
DuplicateProjectConfiguration::COPY_NOTIFICATION_ACTIONS,
|
||||||
|
]),
|
||||||
|
'copy_areas' => [DuplicateProjectConfiguration::COPY_NOTIFICATION_ACTIONS],
|
||||||
|
'name' => 'Alert Template',
|
||||||
|
]);
|
||||||
|
$project->update(['project_template_id' => $template->id]);
|
||||||
|
|
||||||
$this->syncProjectPermissions($project, $user, ['edit']);
|
$this->syncProjectPermissions($project, $user, ['edit']);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
@@ -72,6 +83,8 @@ class ProjectNotificationSettingsControllerTest extends TestCase
|
|||||||
->where('templateVariables.6.value', '$actor_name')
|
->where('templateVariables.6.value', '$actor_name')
|
||||||
->where('templateVariables.7.value', '$assignee_from')
|
->where('templateVariables.7.value', '$assignee_from')
|
||||||
->where('templateVariables.8.value', '$assignee_to')
|
->where('templateVariables.8.value', '$assignee_to')
|
||||||
|
->where('templateState.templateName', 'Alert Template')
|
||||||
|
->where('templateState.inheritedAreas.0.value', DuplicateProjectConfiguration::COPY_NOTIFICATION_ACTIONS)
|
||||||
->missing('notificationActions.0.delivery_url'),
|
->missing('notificationActions.0.delivery_url'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
namespace Tests\Feature\Projects;
|
namespace Tests\Feature\Projects;
|
||||||
|
|
||||||
use App\Actions\Git\GitProviderKey;
|
use App\Actions\Git\GitProviderKey;
|
||||||
|
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectTemplate;
|
||||||
use App\Models\RepositoryConnection;
|
use App\Models\RepositoryConnection;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserGitConnection;
|
use App\Models\UserGitConnection;
|
||||||
@@ -36,6 +38,15 @@ class ProjectRepositorySettingsControllerTest extends TestCase
|
|||||||
'repository_url' => 'https://git.example.test/core-team/kanbangit',
|
'repository_url' => 'https://git.example.test/core-team/kanbangit',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$template = ProjectTemplate::factory()->create([
|
||||||
|
'configuration_snapshot' => app(DuplicateProjectConfiguration::class)->export($project, [
|
||||||
|
DuplicateProjectConfiguration::COPY_REPOSITORY_CONNECTIONS,
|
||||||
|
]),
|
||||||
|
'copy_areas' => [DuplicateProjectConfiguration::COPY_REPOSITORY_CONNECTIONS],
|
||||||
|
'name' => 'Connected Template',
|
||||||
|
]);
|
||||||
|
$project->update(['project_template_id' => $template->id]);
|
||||||
|
|
||||||
$this->syncProjectPermissions($project, $user, ['edit']);
|
$this->syncProjectPermissions($project, $user, ['edit']);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
@@ -46,7 +57,9 @@ class ProjectRepositorySettingsControllerTest extends TestCase
|
|||||||
->where('project.id', $project->id)
|
->where('project.id', $project->id)
|
||||||
->where('repositoryConnections.0.repository_full_name', 'core-team/kanbangit')
|
->where('repositoryConnections.0.repository_full_name', 'core-team/kanbangit')
|
||||||
->where('repositoryConnections.0.has_personal_access', false)
|
->where('repositoryConnections.0.has_personal_access', false)
|
||||||
->where('availableRepositorySources', []),
|
->where('availableRepositorySources', [])
|
||||||
|
->where('templateState.templateName', 'Connected Template')
|
||||||
|
->where('templateState.inheritedAreas.0.value', DuplicateProjectConfiguration::COPY_REPOSITORY_CONNECTIONS),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
namespace Tests\Feature\Projects;
|
namespace Tests\Feature\Projects;
|
||||||
|
|
||||||
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
|
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
|
||||||
|
use App\Actions\Projects\DuplicateProjectConfiguration;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectTemplate;
|
||||||
use App\Models\Ticket;
|
use App\Models\Ticket;
|
||||||
use App\Models\TicketFieldDefinition;
|
use App\Models\TicketFieldDefinition;
|
||||||
use App\Models\TicketLinkType;
|
use App\Models\TicketLinkType;
|
||||||
@@ -37,6 +39,24 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
|
|||||||
'position' => 1,
|
'position' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$template = ProjectTemplate::factory()->create([
|
||||||
|
'configuration_snapshot' => app(DuplicateProjectConfiguration::class)->export($project, [
|
||||||
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
||||||
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES,
|
||||||
|
DuplicateProjectConfiguration::COPY_TICKET_LINK_TYPES,
|
||||||
|
]),
|
||||||
|
'copy_areas' => [
|
||||||
|
DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS,
|
||||||
|
DuplicateProjectConfiguration::COPY_TICKET_TYPES,
|
||||||
|
DuplicateProjectConfiguration::COPY_TICKET_LINK_TYPES,
|
||||||
|
],
|
||||||
|
'name' => 'Issue Tracker Template',
|
||||||
|
]);
|
||||||
|
$project->update([
|
||||||
|
'project_template_id' => $template->id,
|
||||||
|
'show_story_points' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
$this->syncProjectPermissions($project, $user, ['edit']);
|
$this->syncProjectPermissions($project, $user, ['edit']);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
@@ -45,12 +65,14 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
|
|||||||
->assertInertia(fn (Assert $page) => $page
|
->assertInertia(fn (Assert $page) => $page
|
||||||
->component('projects/settings/Metadata')
|
->component('projects/settings/Metadata')
|
||||||
->where('project.show_author', true)
|
->where('project.show_author', true)
|
||||||
->where('project.show_story_points', true)
|
->where('project.show_story_points', false)
|
||||||
->where('ticketTypes.0.name', 'Task')
|
->where('ticketTypes.0.name', 'Task')
|
||||||
->where('ticketTypes.1.name', 'Release')
|
->where('ticketTypes.1.name', 'Release')
|
||||||
->where('ticketTypes.1.is_parent', true)
|
->where('ticketTypes.1.is_parent', true)
|
||||||
->where('ticketTypes.2.name', 'Bug')
|
->where('ticketTypes.2.name', 'Bug')
|
||||||
->where('ticketLinkTypes.0.label', 'Relates to'),
|
->where('ticketLinkTypes.0.label', 'Relates to')
|
||||||
|
->where('templateState.templateName', 'Issue Tracker Template')
|
||||||
|
->where('templateState.overriddenAreas.0.value', DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user