merge: dev conflict resolution

This commit is contained in:
Ahmed Darrazi 2026-01-18 15:47:13 +01:00
commit af17875b9d
15 changed files with 47 additions and 32 deletions

View File

@ -272,7 +272,7 @@ public function mount(): void
); );
}); });
$this->dispatch(OpsUxBrowserEvents::RunEnqueued); OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')

View File

@ -156,7 +156,10 @@ protected function getHeaderActions(): array
); );
if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'], true)) { if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'], true)) {
OperationUxPresenter::queuedToast((string) $opRun->type) Notification::make()
->title('Inventory sync already active')
->body('This operation is already queued or running.')
->warning()
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View Run') ->label('View Run')
@ -168,7 +171,6 @@ protected function getHeaderActions(): array
return; return;
} }
// ----------------------------------------------
// Legacy checks (kept for safety if parallel usage needs it, though OpRun handles idempotency now) // Legacy checks (kept for safety if parallel usage needs it, though OpRun handles idempotency now)
$existing = InventorySyncRun::query() $existing = InventorySyncRun::query()

View File

@ -52,9 +52,9 @@ public function table(Table $table): Table
TextColumn::make('status') TextColumn::make('status')
->badge() ->badge()
->colors([ ->colors([
'warning' => 'queued', 'secondary' => 'queued',
'info' => 'running', 'warning' => 'running',
'secondary' => 'completed', 'success' => 'completed',
]), ]),
TextColumn::make('outcome') TextColumn::make('outcome')

View File

@ -685,7 +685,7 @@ public static function table(Table $table): Table
$operationRunService->dispatchOrFail($operationRun, function () use ($run, $bulkRun, $operationRun): void { $operationRunService->dispatchOrFail($operationRun, function () use ($run, $bulkRun, $operationRun): void {
Bus::dispatch(new RunBackupScheduleJob($run->id, $bulkRun?->id, $operationRun)); Bus::dispatch(new RunBackupScheduleJob($run->id, $bulkRun?->id, $operationRun));
}); }, emitQueuedNotification: false);
} }
$notification = Notification::make() $notification = Notification::make()
@ -825,7 +825,7 @@ public static function table(Table $table): Table
$operationRunService->dispatchOrFail($operationRun, function () use ($run, $bulkRun, $operationRun): void { $operationRunService->dispatchOrFail($operationRun, function () use ($run, $bulkRun, $operationRun): void {
Bus::dispatch(new RunBackupScheduleJob($run->id, $bulkRun?->id, $operationRun)); Bus::dispatch(new RunBackupScheduleJob($run->id, $bulkRun?->id, $operationRun));
}); }, emitQueuedNotification: false);
} }
$notification = Notification::make() $notification = Notification::make()

View File

@ -210,7 +210,7 @@ public function table(Table $table): Table
); );
}); });
$this->dispatch(OpsUxBrowserEvents::RunEnqueued); OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
@ -304,17 +304,13 @@ public function table(Table $table): Table
); );
}); });
$this->dispatch(OpsUxBrowserEvents::RunEnqueued); OpsUxBrowserEvents::dispatchRunEnqueued($this);
Notification::make() OperationUxPresenter::queuedToast((string) $opRun->type)
->title('Removal queued')
->body('A background job has been queued. You can monitor progress in the run details or progress widget.')
->success()
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('View run')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->sendToDatabase($user)
->send(); ->send();
$this->resetTable(); $this->resetTable();

View File

@ -256,9 +256,9 @@ public static function getPages(): array
private static function statusColor(?string $status): string private static function statusColor(?string $status): string
{ {
return match ($status) { return match ($status) {
'queued' => 'warning', 'queued' => 'secondary',
'running' => 'info', 'running' => 'warning',
'completed' => 'secondary', 'completed' => 'success',
default => 'gray', default => 'gray',
}; };
} }

View File

@ -435,7 +435,6 @@ public static function table(Table $table): Table
policyIds: [(int) $record->getKey()], policyIds: [(int) $record->getKey()],
operationRun: $opRun operationRun: $opRun
); );
OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
@ -635,7 +634,6 @@ public static function table(Table $table): Table
policyIds: $ids, policyIds: $ids,
operationRun: $opRun operationRun: $opRun
); );
OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([

View File

@ -90,7 +90,6 @@ protected function getHeaderActions(): array
operationRun: $opRun operationRun: $opRun
); );
}); });
OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([

View File

@ -210,7 +210,7 @@ public static function table(Table $table): Table
initiator: auth()->user() initiator: auth()->user()
); );
if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'])) { if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'], true)) {
Notification::make() Notification::make()
->title('Policy sync already active') ->title('Policy sync already active')
->body('This operation is already queued or running.') ->body('This operation is already queued or running.')

View File

@ -28,7 +28,6 @@ public function toDatabase(object $notifiable): array
return OperationUxPresenter::terminalDatabaseNotification( return OperationUxPresenter::terminalDatabaseNotification(
run: $this->run, run: $this->run,
tenant: $tenant instanceof Tenant ? $tenant : null, tenant: $tenant instanceof Tenant ? $tenant : null,
) )->getDatabaseMessage();
->getDatabaseMessage();
} }
} }

View File

@ -20,7 +20,6 @@
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Blade;
use Illuminate\View\Middleware\ShareErrorsFromSession; use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider class AdminPanelProvider extends PanelProvider
@ -42,7 +41,7 @@ public function panel(Panel $panel): Panel
->renderHook( ->renderHook(
PanelsRenderHook::BODY_END, PanelsRenderHook::BODY_END,
fn () => (bool) config('tenantpilot.bulk_operations.progress_widget_enabled', true) fn () => (bool) config('tenantpilot.bulk_operations.progress_widget_enabled', true)
? Blade::render("@livewire('bulk-operation-progress', [], key('ops-ux-progress'))") ? view('livewire.bulk-operation-progress-wrapper')->render()
: '' : ''
) )
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') ->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')

View File

@ -6,6 +6,7 @@
use App\Models\Tenant; use App\Models\Tenant;
use App\Models\User; use App\Models\User;
use App\Notifications\OperationRunCompleted as OperationRunCompletedNotification; use App\Notifications\OperationRunCompleted as OperationRunCompletedNotification;
use App\Notifications\OperationRunQueued as OperationRunQueuedNotification;
use App\Support\OperationRunOutcome; use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus; use App\Support\OperationRunStatus;
use App\Support\OpsUx\SummaryCountsNormalizer; use App\Support\OpsUx\SummaryCountsNormalizer;
@ -142,10 +143,14 @@ public function updateRun(
* If dispatch fails synchronously (misconfiguration, serialization errors, etc.), * If dispatch fails synchronously (misconfiguration, serialization errors, etc.),
* the OperationRun is marked terminal failed so we do not leave a misleading queued run behind. * the OperationRun is marked terminal failed so we do not leave a misleading queued run behind.
*/ */
public function dispatchOrFail(OperationRun $run, callable $dispatcher): void public function dispatchOrFail(OperationRun $run, callable $dispatcher, bool $emitQueuedNotification = true): void
{ {
try { try {
$dispatcher(); $dispatcher();
if ($emitQueuedNotification && $run->wasRecentlyCreated && $run->user instanceof User) {
$run->user->notify(new OperationRunQueuedNotification($run));
}
} catch (Throwable $e) { } catch (Throwable $e) {
$this->updateRun( $this->updateRun(
$run, $run,

View File

@ -2,11 +2,12 @@
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Notifications\OperationRunCompleted; use App\Notifications\OperationRunCompleted;
use App\Notifications\OperationRunQueued;
use App\Services\OperationRunService; use App\Services\OperationRunService;
use App\Support\OperationRunLinks; use App\Support\OperationRunLinks;
use Filament\Facades\Filament; use Filament\Facades\Filament;
it('does not emit a queued database notification after successful dispatch (toast-only)', function () { it('emits a queued notification after successful dispatch (initiator only) with view link', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user); $this->actingAs($user);
@ -25,7 +26,18 @@
// no-op (dispatch succeeded) // no-op (dispatch succeeded)
}); });
expect($user->notifications()->count())->toBe(0); $this->assertDatabaseHas('notifications', [
'notifiable_id' => $user->getKey(),
'notifiable_type' => $user->getMorphClass(),
'type' => OperationRunQueued::class,
'data->format' => 'filament',
'data->title' => 'Policy sync queued',
]);
$notification = $user->notifications()->latest('id')->first();
expect($notification)->not->toBeNull();
expect($notification->data['actions'][0]['url'] ?? null)
->toBe(OperationRunLinks::view($run, $tenant));
}); });
it('does not emit queued notifications for runs without an initiator', function () { it('does not emit queued notifications for runs without an initiator', function () {

View File

@ -138,7 +138,6 @@
$fresh = $run->fresh(); $fresh = $run->fresh();
expect($fresh?->status)->toBe('running'); expect($fresh?->status)->toBe('running');
expect($fresh?->started_at)->not->toBeNull(); expect($fresh?->started_at)->not->toBeNull();
$service->updateRun($run, 'completed', 'succeeded', ['succeeded' => 1]); $service->updateRun($run, 'completed', 'succeeded', ['succeeded' => 1]);
$fresh = $run->fresh(); $fresh = $run->fresh();

View File

@ -2,10 +2,11 @@
declare(strict_types=1); declare(strict_types=1);
use App\Notifications\OperationRunQueued;
use App\Services\OperationRunService; use App\Services\OperationRunService;
use Filament\Facades\Filament; use Filament\Facades\Filament;
it('does not emit database notifications when dispatching a queued operation', function (): void { it('emits at most one queued database notification per newly created run', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user); $this->actingAs($user);
@ -26,5 +27,10 @@
// no-op (dispatch succeeded) // no-op (dispatch succeeded)
}); });
expect($user->notifications()->count())->toBe(0); expect($user->notifications()->count())->toBe(1);
$this->assertDatabaseHas('notifications', [
'notifiable_id' => $user->getKey(),
'notifiable_type' => $user->getMorphClass(),
'type' => OperationRunQueued::class,
]);
})->group('ops-ux'); })->group('ops-ux');