spec: refine 057 + extend 058 #67

Merged
ahmido merged 28 commits from 058-tenant-ui-polish into dev 2026-01-21 11:29:42 +00:00
15 changed files with 47 additions and 32 deletions
Showing only changes of commit af17875b9d - Show all commits

View File

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

View File

@ -156,7 +156,10 @@ protected function getHeaderActions(): array
);
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([
Action::make('view_run')
->label('View Run')
@ -168,7 +171,6 @@ protected function getHeaderActions(): array
return;
}
// ----------------------------------------------
// Legacy checks (kept for safety if parallel usage needs it, though OpRun handles idempotency now)
$existing = InventorySyncRun::query()

View File

@ -52,9 +52,9 @@ public function table(Table $table): Table
TextColumn::make('status')
->badge()
->colors([
'warning' => 'queued',
'info' => 'running',
'secondary' => 'completed',
'secondary' => 'queued',
'warning' => 'running',
'success' => 'completed',
]),
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 {
Bus::dispatch(new RunBackupScheduleJob($run->id, $bulkRun?->id, $operationRun));
});
}, emitQueuedNotification: false);
}
$notification = Notification::make()
@ -825,7 +825,7 @@ public static function table(Table $table): Table
$operationRunService->dispatchOrFail($operationRun, function () use ($run, $bulkRun, $operationRun): void {
Bus::dispatch(new RunBackupScheduleJob($run->id, $bulkRun?->id, $operationRun));
});
}, emitQueuedNotification: false);
}
$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)
->actions([
Actions\Action::make('view_run')
@ -304,17 +304,13 @@ public function table(Table $table): Table
);
});
$this->dispatch(OpsUxBrowserEvents::RunEnqueued);
Notification::make()
->title('Removal queued')
->body('A background job has been queued. You can monitor progress in the run details or progress widget.')
->success()
OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($opRun, $tenant)),
])
->sendToDatabase($user)
->send();
$this->resetTable();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,6 @@
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Blade;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
@ -42,7 +41,7 @@ public function panel(Panel $panel): Panel
->renderHook(
PanelsRenderHook::BODY_END,
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')

View File

@ -6,6 +6,7 @@
use App\Models\Tenant;
use App\Models\User;
use App\Notifications\OperationRunCompleted as OperationRunCompletedNotification;
use App\Notifications\OperationRunQueued as OperationRunQueuedNotification;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OpsUx\SummaryCountsNormalizer;
@ -142,10 +143,14 @@ public function updateRun(
* If dispatch fails synchronously (misconfiguration, serialization errors, etc.),
* 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 {
$dispatcher();
if ($emitQueuedNotification && $run->wasRecentlyCreated && $run->user instanceof User) {
$run->user->notify(new OperationRunQueuedNotification($run));
}
} catch (Throwable $e) {
$this->updateRun(
$run,

View File

@ -2,11 +2,12 @@
use App\Models\OperationRun;
use App\Notifications\OperationRunCompleted;
use App\Notifications\OperationRunQueued;
use App\Services\OperationRunService;
use App\Support\OperationRunLinks;
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');
$this->actingAs($user);
@ -25,7 +26,18 @@
// 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 () {

View File

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

View File

@ -2,10 +2,11 @@
declare(strict_types=1);
use App\Notifications\OperationRunQueued;
use App\Services\OperationRunService;
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');
$this->actingAs($user);
@ -26,5 +27,10 @@
// 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');