$guidance * @param array{ * review:?string, * evidence:?string, * operation:?string, * download:?string, * successor_review:?string * } $urls * @param array{ * can_manage_review?:bool, * successor_review_status?:?string * } $execution * @return array{ * primary_action:array{ * key:string, * label:string, * type:string, * url:?string, * icon:string, * kind:string, * action_name:?string, * capability:?string, * requires_confirmation:bool, * audit_event:?string, * operation_run_type:?string, * disabled_reason:?string * }, * secondary_actions:list * } */ public static function map( EnvironmentReview $review, array $guidance, string $sourceSurface, array $urls, array $execution = [], ): array { $fallbackPrimary = ResolutionAction::fromArray( is_array($guidance['primary_action'] ?? null) ? $guidance['primary_action'] : null, self::caseKey((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)).'.primary_action', __('localization.review.review_output_limitations'), ); $fallbackSecondary = self::secondaryActions($guidance); $candidatePrimary = self::candidatePrimaryAction($review, $guidance, $sourceSurface, $urls, $execution); if ($candidatePrimary === null) { return [ 'primary_action' => $fallbackPrimary, 'secondary_actions' => $fallbackSecondary, ]; } $primaryAction = ResolutionAction::fromArray( $candidatePrimary, self::caseKey((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)).'.primary_action', __('localization.review.review_output_limitations'), ); $secondaryActions = self::deduplicateActions(array_merge( self::shouldPromoteFallbackPrimary($fallbackPrimary, $primaryAction) ? [$fallbackPrimary] : [], $fallbackSecondary, ), $primaryAction['key']); return [ 'primary_action' => $primaryAction, 'secondary_actions' => $secondaryActions, ]; } /** * @param array $guidance * @param array{ * review:?string, * evidence:?string, * operation:?string, * download:?string, * successor_review:?string * } $urls * @param array{ * can_manage_review?:bool, * successor_review_status?:?string * } $execution * @return array{ * key:string, * label:string, * type:string, * url:?string, * icon:string, * kind:string, * action_name:?string, * capability:?string, * requires_confirmation:bool, * audit_event:?string, * operation_run_type:?string * }|null */ private static function candidatePrimaryAction( EnvironmentReview $review, array $guidance, string $sourceSurface, array $urls, array $execution, ): ?array { $state = (string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN); $canManageReview = (bool) ($execution['can_manage_review'] ?? false); $successorReviewUrl = self::normalizedUrl($urls['successor_review'] ?? null); $successorReviewStatus = EnvironmentReviewStatus::tryFrom((string) ($execution['successor_review_status'] ?? '')); if ($successorReviewUrl !== null) { return [ 'key' => 'open_successor_review', 'label' => $successorReviewStatus?->isMutable() ? __('localization.review.open_draft_review') : __('localization.review.open_successor_review'), 'type' => ResolutionAction::TYPE_NAVIGATION, 'url' => $successorReviewUrl, 'icon' => 'heroicon-o-arrow-top-right-on-square', 'kind' => 'environment_link', 'action_name' => $sourceSurface === 'environment_review_detail' ? 'open_successor_review' : null, 'capability' => null, 'requires_confirmation' => false, 'audit_event' => null, 'operation_run_type' => null, ]; } if ($review->statusEnum() === EnvironmentReviewStatus::Ready) { if (! $canManageReview || ! self::supportsExecutableAction($sourceSurface, 'publish_review')) { return null; } return [ 'key' => 'publish_review', 'label' => __('localization.review.publish_review'), 'type' => ResolutionAction::TYPE_DOMAIN_ACTION, 'url' => null, 'icon' => 'heroicon-o-check-badge', 'kind' => 'environment_action', 'action_name' => 'publish_review', 'capability' => Capabilities::ENVIRONMENT_REVIEW_MANAGE, 'requires_confirmation' => true, 'audit_event' => AuditActionId::EnvironmentReviewPublished->value, 'operation_run_type' => null, ]; } if ($review->isMutable() && $state !== ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY) { if (! $canManageReview || ! self::supportsExecutableAction($sourceSurface, 'refresh_review')) { return null; } return [ 'key' => 'refresh_review', 'label' => __('localization.review.refresh_review'), 'type' => ResolutionAction::TYPE_OPERATION_ACTION, 'url' => null, 'icon' => 'heroicon-o-arrow-path', 'kind' => 'environment_action', 'action_name' => 'refresh_review', 'capability' => Capabilities::ENVIRONMENT_REVIEW_MANAGE, 'requires_confirmation' => true, 'audit_event' => AuditActionId::EnvironmentReviewRefreshed->value, 'operation_run_type' => OperationRunType::EnvironmentReviewCompose->value, ]; } if (! $review->isPublished() || ! in_array($state, [ ReviewPackOutputResolutionGuidance::STATE_PUBLICATION_BLOCKED, ReviewPackOutputResolutionGuidance::STATE_PUBLISHED_WITH_LIMITATIONS, ReviewPackOutputResolutionGuidance::STATE_EXPORT_NOT_READY, ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY, ], true)) { return null; } if (! $canManageReview || ! self::supportsExecutableAction($sourceSurface, 'create_next_review')) { return null; } return [ 'key' => 'create_next_review', 'label' => __('localization.review.create_next_review'), 'type' => ResolutionAction::TYPE_OPERATION_ACTION, 'url' => null, 'icon' => 'heroicon-o-document-duplicate', 'kind' => 'environment_action', 'action_name' => $sourceSurface === 'customer_review_workspace' ? 'createNextReview' : 'create_next_review', 'capability' => Capabilities::ENVIRONMENT_REVIEW_MANAGE, 'requires_confirmation' => true, 'audit_event' => AuditActionId::EnvironmentReviewSuccessorCreated->value, 'operation_run_type' => OperationRunType::EnvironmentReviewCompose->value, ]; } private static function supportsExecutableAction(string $sourceSurface, string $actionName): bool { return match ($sourceSurface) { 'customer_review_workspace' => $actionName === 'create_next_review', 'environment_review_detail' => in_array($actionName, [ 'refresh_review', 'publish_review', 'create_next_review', ], true), default => false, }; } /** * @param array $guidance * @return list */ private static function secondaryActions(array $guidance): array { $secondaryActions = is_array($guidance['secondary_actions'] ?? null) ? $guidance['secondary_actions'] : []; return array_values(array_map( static fn (array $action, int $index): array => ResolutionAction::fromArray( $action, self::caseKey((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)).'.secondary_action_'.$index, __('localization.review.review_output_limitations'), ), array_values(array_filter($secondaryActions, static fn (mixed $action): bool => is_array($action))), array_keys(array_values(array_filter($secondaryActions, static fn (mixed $action): bool => is_array($action)))), )); } /** * @param list $actions * @return list */ private static function deduplicateActions(array $actions, string $primaryKey): array { $seen = [$primaryKey => true]; $deduplicated = []; foreach ($actions as $action) { if (($action['type'] ?? ResolutionAction::TYPE_NONE) === ResolutionAction::TYPE_NONE) { continue; } $key = (string) ($action['key'] ?? ''); if ($key === '' || isset($seen[$key])) { continue; } $seen[$key] = true; $deduplicated[] = $action; } return $deduplicated; } /** * @param array{ * key:string, * label:string, * type:string, * url:?string, * icon:string, * kind:string, * action_name:?string, * capability:?string, * requires_confirmation:bool, * audit_event:?string, * operation_run_type:?string, * disabled_reason:?string * } $fallbackPrimary * @param array{ * key:string, * label:string, * type:string, * url:?string, * icon:string, * kind:string, * action_name:?string, * capability:?string, * requires_confirmation:bool, * audit_event:?string, * operation_run_type:?string, * disabled_reason:?string * } $primaryAction */ private static function shouldPromoteFallbackPrimary(array $fallbackPrimary, array $primaryAction): bool { if ($fallbackPrimary['key'] === $primaryAction['key']) { return false; } return $fallbackPrimary['type'] !== ResolutionAction::TYPE_NONE && ($fallbackPrimary['url'] !== null || $fallbackPrimary['action_name'] !== null); } private static function normalizedUrl(mixed $url): ?string { return is_string($url) && trim($url) !== '' ? trim($url) : null; } private static function caseKey(string $state): string { return 'review_output.'.$state; } }