*/ public static function copyableAreas(): array { return [ self::COPY_PROJECT_SETTINGS, self::COPY_BOARD_WORKFLOW, self::COPY_TICKET_TYPES, self::COPY_TICKET_PRIORITIES, self::COPY_TICKET_FIELDS, self::COPY_TICKET_LINK_TYPES, self::COPY_REPOSITORY_CONNECTIONS, self::COPY_NOTIFICATION_ACTIONS, ]; } /** * @return list */ public static function copyAreaOptions(): array { return [ [ 'value' => self::COPY_PROJECT_SETTINGS, 'label' => 'Project settings', 'description' => 'Copy general project preferences like story points and author visibility.', ], [ 'value' => self::COPY_BOARD_WORKFLOW, 'label' => 'Board workflow', 'description' => 'Copy board columns, ticket statuses, and allowed status transitions.', ], [ 'value' => self::COPY_TICKET_TYPES, 'label' => 'Ticket types', 'description' => 'Copy available ticket types, including parent-ticket types.', ], [ 'value' => self::COPY_TICKET_PRIORITIES, 'label' => 'Ticket priorities', 'description' => 'Copy the project-specific ticket priorities.', ], [ 'value' => self::COPY_TICKET_FIELDS, 'label' => 'Custom ticket fields', 'description' => 'Copy custom ticket field definitions without any ticket values.', ], [ 'value' => self::COPY_TICKET_LINK_TYPES, 'label' => 'Ticket link types', 'description' => 'Copy available relationship labels between tickets.', ], [ 'value' => self::COPY_REPOSITORY_CONNECTIONS, 'label' => 'Repository connections', 'description' => 'Copy linked repositories without changing the new project identity.', ], [ 'value' => self::COPY_NOTIFICATION_ACTIONS, 'label' => 'Notification actions', 'description' => 'Copy notification rules and delivery settings.', ], ]; } /** * @param list $copyAreas */ public function handle(Project $sourceProject, Project $targetProject, array $copyAreas): void { $selectedAreas = array_values(array_intersect(self::copyableAreas(), array_unique($copyAreas))); if ($selectedAreas === []) { return; } $sourceProject->loadMissing([ 'boardColumns', 'notificationActions', 'repositoryConnections', 'ticketFieldDefinitions', 'ticketLinkTypes', 'ticketPriorities', 'ticketStatuses.allowedTargetStatuses', 'ticketTypes', ]); if (in_array(self::COPY_PROJECT_SETTINGS, $selectedAreas, true)) { $targetProject->forceFill([ 'show_author' => $sourceProject->show_author, 'show_story_points' => $sourceProject->show_story_points, ])->save(); } if (in_array(self::COPY_BOARD_WORKFLOW, $selectedAreas, true)) { $targetProject->ticketStatuses()->delete(); $targetProject->boardColumns()->delete(); $boardColumnIdMap = []; foreach ($sourceProject->boardColumns as $boardColumn) { $newBoardColumn = $targetProject->boardColumns()->create([ 'name' => $boardColumn->name, 'position' => $boardColumn->position, ]); $boardColumnIdMap[$boardColumn->id] = $newBoardColumn->id; } $ticketStatusIdMap = []; foreach ($sourceProject->ticketStatuses as $ticketStatus) { $newTicketStatus = $targetProject->ticketStatuses()->create([ 'board_column_id' => $ticketStatus->board_column_id === null ? null : ($boardColumnIdMap[$ticketStatus->board_column_id] ?? null), 'name' => $ticketStatus->name, 'position' => $ticketStatus->position, ]); $ticketStatusIdMap[$ticketStatus->id] = $newTicketStatus->id; } foreach ($sourceProject->ticketStatuses as $ticketStatus) { $newTicketStatusId = $ticketStatusIdMap[$ticketStatus->id] ?? null; if ($newTicketStatusId === null) { continue; } $targetProject->ticketStatuses() ->findOrFail($newTicketStatusId) ->allowedTargetStatuses() ->sync( $ticketStatus->allowedTargetStatuses ->pluck('id') ->map(fn (int $statusId): ?int => $ticketStatusIdMap[$statusId] ?? null) ->filter() ->values() ->all(), ); } } if (in_array(self::COPY_TICKET_TYPES, $selectedAreas, true)) { $targetProject->ticketTypes()->delete(); foreach ($sourceProject->ticketTypes as $ticketType) { $targetProject->ticketTypes()->create([ 'name' => $ticketType->name, 'position' => $ticketType->position, 'is_parent' => $ticketType->is_parent, ]); } } if (in_array(self::COPY_TICKET_PRIORITIES, $selectedAreas, true)) { $targetProject->ticketPriorities()->delete(); foreach ($sourceProject->ticketPriorities as $ticketPriority) { $targetProject->ticketPriorities()->create([ 'name' => $ticketPriority->name, 'position' => $ticketPriority->position, ]); } } if (in_array(self::COPY_TICKET_FIELDS, $selectedAreas, true)) { $targetProject->ticketFieldDefinitions()->delete(); foreach ($sourceProject->ticketFieldDefinitions as $ticketFieldDefinition) { $targetProject->ticketFieldDefinitions()->create([ 'field_type' => $ticketFieldDefinition->field_type, 'label' => $ticketFieldDefinition->label, 'position' => $ticketFieldDefinition->position, ]); } } if (in_array(self::COPY_TICKET_LINK_TYPES, $selectedAreas, true)) { $targetProject->ticketLinkTypes()->delete(); foreach ($sourceProject->ticketLinkTypes as $ticketLinkType) { $targetProject->ticketLinkTypes()->create([ 'label' => $ticketLinkType->label, 'counterpart_label' => $ticketLinkType->counterpart_label, 'position' => $ticketLinkType->position, ]); } } if (in_array(self::COPY_REPOSITORY_CONNECTIONS, $selectedAreas, true)) { $targetProject->repositoryConnections()->delete(); foreach ($sourceProject->repositoryConnections as $repositoryConnection) { $targetProject->repositoryConnections()->create([ 'provider' => $repositoryConnection->provider, 'base_url' => $repositoryConnection->base_url, 'provider_repository_id' => $repositoryConnection->provider_repository_id, 'owner_name' => $repositoryConnection->owner_name, 'repository_name' => $repositoryConnection->repository_name, 'repository_full_name' => $repositoryConnection->repository_full_name, 'repository_url' => $repositoryConnection->repository_url, ]); } } if (in_array(self::COPY_NOTIFICATION_ACTIONS, $selectedAreas, true)) { $targetProject->notificationActions()->delete(); foreach ($sourceProject->notificationActions as $notificationAction) { $targetProject->notificationActions()->create([ 'name' => $notificationAction->name, 'enabled' => $notificationAction->enabled, 'event_type' => $notificationAction->event_type, 'channel' => $notificationAction->channel, 'channel_configuration' => $notificationAction->channel_configuration, 'message_template' => $notificationAction->message_template, 'delivery_url' => $notificationAction->delivery_url, 'filters' => $notificationAction->filters, ]); } } } }