057-filament-v5-upgrade #66
@ -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')
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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([
|
||||||
|
|||||||
@ -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([
|
||||||
|
|||||||
@ -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.')
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 () {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user