diff --git a/app/Console/Commands/ReviewIntegrationRequestsCommand.php b/app/Console/Commands/ReviewIntegrationRequestsCommand.php index 9de6661f..86a2f9fe 100644 --- a/app/Console/Commands/ReviewIntegrationRequestsCommand.php +++ b/app/Console/Commands/ReviewIntegrationRequestsCommand.php @@ -42,7 +42,7 @@ class ReviewIntegrationRequestsCommand extends Command foreach ($pending as $request) { $decision = $this->choice( "Review \"{$request->name}\" ({$request->url})", - ['approve', 'in progress', 'reject', 'not doable', 'skip'], + ['approve', 'in progress', 'reject', 'not doable', 'done', 'skip'], 'skip', ); @@ -78,7 +78,7 @@ class ReviewIntegrationRequestsCommand extends Command $this->apply($request, $this->choice( "New status for \"{$request->name}\"", - ['approve', 'in progress', 'reject', 'not doable'], + ['approve', 'in progress', 'reject', 'not doable', 'done'], )); $this->info("\"{$request->name}\" is now {$request->status->label()}."); @@ -93,6 +93,7 @@ class ReviewIntegrationRequestsCommand extends Command 'in progress' => IntegrationRequestStatus::InProgress, 'reject' => IntegrationRequestStatus::Rejected, 'not doable' => IntegrationRequestStatus::NotDoable, + 'done' => IntegrationRequestStatus::Done, default => null, }; @@ -124,7 +125,7 @@ class ReviewIntegrationRequestsCommand extends Command return $this->ask('Add a comment for this request (optional, shown to users)') ?: null; } - // Approving, rejecting or re-queuing drops any stale public comment. + // Approving, rejecting, re-queuing or marking done drops any stale public comment. return null; } @@ -157,7 +158,7 @@ class ReviewIntegrationRequestsCommand extends Command $request->name, $request->url, $request->status->label(), - $request->user->email, + $request->user?->email ?? '—', $request->votes_count, $request->created_at?->format('Y-m-d') ?? '—', ]; diff --git a/app/Enums/IntegrationRequestStatus.php b/app/Enums/IntegrationRequestStatus.php index 642b233f..e66032d6 100644 --- a/app/Enums/IntegrationRequestStatus.php +++ b/app/Enums/IntegrationRequestStatus.php @@ -9,6 +9,7 @@ enum IntegrationRequestStatus: string case InProgress = 'in_progress'; case Rejected = 'rejected'; case NotDoable = 'not_doable'; + case Done = 'done'; public function label(): string { @@ -18,6 +19,7 @@ enum IntegrationRequestStatus: string self::InProgress => 'In progress', self::Rejected => 'Rejected', self::NotDoable => 'Not doable', + self::Done => 'Done', }; } } diff --git a/app/Http/Controllers/IntegrationRequestController.php b/app/Http/Controllers/IntegrationRequestController.php index 3471a54c..b090bd48 100644 --- a/app/Http/Controllers/IntegrationRequestController.php +++ b/app/Http/Controllers/IntegrationRequestController.php @@ -79,8 +79,8 @@ class IntegrationRequestController extends Controller { $user = $request->user(); - // Not-doable requests are frozen: their tally can no longer be touched. - if ($integrationRequest->status === IntegrationRequestStatus::NotDoable) { + // Closed requests (not-doable or done) are frozen: their tally can no longer be touched. + if (in_array($integrationRequest->status, [IntegrationRequestStatus::NotDoable, IntegrationRequestStatus::Done], true)) { abort(404); } @@ -106,7 +106,7 @@ class IntegrationRequestController extends Controller { return IntegrationRequest::query() ->where(function ($query) use ($user) { - $query->whereIn('status', [IntegrationRequestStatus::Approved, IntegrationRequestStatus::InProgress, IntegrationRequestStatus::NotDoable]) + $query->whereIn('status', [IntegrationRequestStatus::Approved, IntegrationRequestStatus::InProgress, IntegrationRequestStatus::NotDoable, IntegrationRequestStatus::Done]) ->orWhere(function ($inner) use ($user) { $inner->where('status', IntegrationRequestStatus::Pending) ->where('user_id', $user->id); @@ -118,8 +118,8 @@ class IntegrationRequestController extends Controller 'votes as can_unvote' => fn ($query) => $query->where('user_id', $user->id) ->where('created_at', '>=', now()->startOfMonth()), ]) - // Not-doable requests sink to the bottom regardless of their votes. - ->orderByRaw('CASE WHEN status = ? THEN 1 ELSE 0 END', [IntegrationRequestStatus::NotDoable->value]) + // Closed requests (not-doable or done) sink to the bottom regardless of their votes. + ->orderByRaw('CASE WHEN status IN (?, ?) THEN 1 ELSE 0 END', [IntegrationRequestStatus::NotDoable->value, IntegrationRequestStatus::Done->value]) ->orderByDesc('votes_count') ->orderByDesc('created_at') ->get(); diff --git a/database/factories/IntegrationRequestFactory.php b/database/factories/IntegrationRequestFactory.php index 0f20a13b..3031ad5b 100644 --- a/database/factories/IntegrationRequestFactory.php +++ b/database/factories/IntegrationRequestFactory.php @@ -47,4 +47,9 @@ class IntegrationRequestFactory extends Factory 'comment' => fake()->sentence(), ]); } + + public function done(): static + { + return $this->state(['status' => IntegrationRequestStatus::Done]); + } } diff --git a/lang/es.json b/lang/es.json index 2e54b37d..783d7ade 100644 --- a/lang/es.json +++ b/lang/es.json @@ -35,6 +35,7 @@ "Pending review": "Pendiente de revisión", "Not doable": "No viable", "In progress": "En proceso", + "Done": "Hecho", "Something went wrong.": "Algo salió mal.", "Integration requests": "Solicitudes de integración", "Request integration": "Solicitar integración", diff --git a/resources/js/components/integration-requests/integration-requests-board.tsx b/resources/js/components/integration-requests/integration-requests-board.tsx index 81db4f11..efcd4db4 100644 --- a/resources/js/components/integration-requests/integration-requests-board.tsx +++ b/resources/js/components/integration-requests/integration-requests-board.tsx @@ -21,7 +21,13 @@ export interface IntegrationRequestItem { id: string; name: string; url: string; - status: 'pending' | 'approved' | 'in_progress' | 'rejected' | 'not_doable'; + status: + | 'pending' + | 'approved' + | 'in_progress' + | 'rejected' + | 'not_doable' + | 'done'; comment: string | null; votes_count: number; has_voted: boolean; @@ -34,6 +40,11 @@ interface BoardPayload { actionsRemaining: number; } +// Closed requests (not-doable or done) are frozen: no more votes in or out. +function isFrozen(status: IntegrationRequestItem['status']): boolean { + return status === 'not_doable' || status === 'done'; +} + interface Props { initialRequests?: IntegrationRequestItem[]; initialActionsRemaining?: number; @@ -115,7 +126,7 @@ export function IntegrationRequestsBoard({ }; const handleVote = async (item: IntegrationRequestItem) => { - if (busy || item.status === 'not_doable' || actionsRemaining <= 0) { + if (busy || isFrozen(item.status) || actionsRemaining <= 0) { return; } @@ -138,7 +149,7 @@ export function IntegrationRequestsBoard({ }; const handleRemoveVote = async (item: IntegrationRequestItem) => { - if (busy || !item.can_unvote || item.status === 'not_doable') { + if (busy || !item.can_unvote || isFrozen(item.status)) { return; } @@ -271,10 +282,13 @@ export function IntegrationRequestsBoard({ {__('Not doable')} )} + {item.status === 'done' && ( + {__('Done')} + )}
{item.can_unvote && - item.status !== 'not_doable' && ( + !isFrozen(item.status) && (