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' && (
+