feat(installer): auto-advance past the migration step (#2283)
## Summary Migration step now moves itself on. Once migrations report nothing pending, a 5-second countdown starts, renders inside the Next button (`Next (5)` → `(1)`), and the wizard footer scrolls into view. Pause sits to the left of Next for anyone who wants to read the log first. It occupies the old Back slot — Back is gone from the whole wizard, since the only thing behind you is a migration that already ran. Installer only. Updater's existing 10s redirect untouched. ## Test plan - `vendor/bin/pest tests/Feature/Installer/ tests/Feature/InstallerAccessTest.php` — 12 passed - `phpstan` on the page — clean - New `MigrationAutoAdvanceTest` asserts the rendered footer keeps the seam with Filament's vendor wizard blade intact: Next stays findable via `data-installer-next`, Pause has replaced Back - Not covered by tests: the countdown running in a browser, and `runMigrations()` itself. That method shells out to `php artisan` via `StreamedCommandsService`, and the subprocess reads `.env` rather than phpunit's forced sqlite — calling it from a test hits the dev database. Comment in the test file records this. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added automatic progression to the next installer step after migrations complete. * Added a visible countdown with controls to pause and resume the transition. * Added automatic scrolling to keep the wizard controls visible during the countdown. * **Tests** * Added coverage for the countdown, navigation controls, and pause/resume behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
commit
667cbc835d
@ -25,6 +25,7 @@ use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\EmbeddedSchema;
|
||||
use Filament\Schemas\Components\Form;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\View as ViewComponent;
|
||||
use Filament\Schemas\Components\Wizard;
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Filament\Schemas\Schema as FilamentSchema;
|
||||
@ -49,6 +50,12 @@ class Installer extends Page
|
||||
{
|
||||
protected static ?string $slug = 'install';
|
||||
|
||||
/**
|
||||
* How long the migration step waits before moving itself on. Long enough to
|
||||
* hit Pause if the log needs a read, short enough not to feel stalled.
|
||||
*/
|
||||
private const int AUTO_ADVANCE_SECONDS = 5;
|
||||
|
||||
public string $stream = 'console_output';
|
||||
|
||||
public ?array $user = null;
|
||||
@ -164,6 +171,36 @@ class Installer extends Page
|
||||
])
|
||||
->startOnStep(fn (): int => $this->computeStartStep())
|
||||
->persistStepInQueryString()
|
||||
->nextAction(fn (Action $action): Action => $action
|
||||
->label(new HtmlString(
|
||||
Blade::render(
|
||||
<<<'BLADE'
|
||||
{{ __('filament-schemas::components.wizard.actions.next_step.label') }}<span
|
||||
x-cloak
|
||||
x-show="$store.installerAutoAdvance?.active"
|
||||
x-text="` (${$store.installerAutoAdvance?.remaining})`"
|
||||
></span>
|
||||
BLADE
|
||||
)
|
||||
))
|
||||
->extraAttributes(['data-installer-next' => 'true']))
|
||||
// The wizard's only backwards step would be onto an already-run
|
||||
// migration log, so the slot carries the countdown's Pause
|
||||
// control instead of a Back button. `.stop` keeps the click from
|
||||
// reaching the footer wrapper, which would otherwise step back.
|
||||
->previousAction(fn (Action $action): Action => $action
|
||||
->label(new HtmlString(
|
||||
Blade::render(
|
||||
<<<'BLADE'
|
||||
<span x-text="$store.installerAutoAdvance?.paused ? @js(__('installer.resume')) : @js(__('installer.pause'))">{{ __('installer.pause') }}</span>
|
||||
BLADE
|
||||
)
|
||||
))
|
||||
->extraAttributes([
|
||||
'x-cloak' => true,
|
||||
'x-show' => '$store.installerAutoAdvance?.active',
|
||||
'x-on:click.stop' => '$store.installerAutoAdvance.toggle()',
|
||||
]))
|
||||
->submitAction(
|
||||
new HtmlString(
|
||||
Blade::render(
|
||||
@ -298,6 +335,16 @@ class Installer extends Page
|
||||
to: $this->stream
|
||||
);
|
||||
|
||||
// Only hand the step over to the countdown once there is genuinely
|
||||
// nothing left to run — otherwise it would advance into the step's own
|
||||
// validation, which halts on pending migrations.
|
||||
if (count(app(MigrationService::class)->migrationsAvailable()) === 0) {
|
||||
$this->dispatch(
|
||||
'installer-migrations-complete',
|
||||
seconds: self::AUTO_ADVANCE_SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
return $this->migrationOutput = $output;
|
||||
}
|
||||
|
||||
@ -500,6 +547,11 @@ class Installer extends Page
|
||||
->viewData([
|
||||
'stream' => $this->stream,
|
||||
]),
|
||||
|
||||
ViewComponent::make('filament.installer.auto-advance')
|
||||
->viewData([
|
||||
'seconds' => self::AUTO_ADVANCE_SECONDS,
|
||||
]),
|
||||
])
|
||||
->afterValidation(function (): void {
|
||||
if (count(app(MigrationService::class)->migrationsAvailable()) > 0) {
|
||||
|
||||
@ -40,6 +40,8 @@ return [
|
||||
'click_update_to_run' => 'Click "Update" to run the script.',
|
||||
'update' => 'Update',
|
||||
'migrations_not_completed' => 'You still have :count migrations to run. Please try again...',
|
||||
'pause' => 'Pause',
|
||||
'resume' => 'Resume',
|
||||
'user_and_airline_setup' => 'User & Airline Setup',
|
||||
'legacy_importer' => 'phpvms v5 Legacy Importer',
|
||||
'super_admin_information' => 'Super Admin User Information',
|
||||
|
||||
87
resources/views/filament/installer/auto-advance.blade.php
Normal file
87
resources/views/filament/installer/auto-advance.blade.php
Normal file
@ -0,0 +1,87 @@
|
||||
{{--
|
||||
Drives the installer's "advance to the next step on its own" countdown.
|
||||
|
||||
The two controls it talks to — the wizard's Pause and Next buttons — are
|
||||
rendered in the wizard footer, which sits outside this step's schema, so the
|
||||
shared countdown state lives in a global Alpine store rather than an x-data
|
||||
scope on a common ancestor.
|
||||
--}}
|
||||
<script>
|
||||
(() => {
|
||||
if (window.installerAutoAdvanceRegistered) {
|
||||
return
|
||||
}
|
||||
|
||||
window.installerAutoAdvanceRegistered = true
|
||||
|
||||
const store = () => window.Alpine?.store('installerAutoAdvance')
|
||||
|
||||
const register = () => {
|
||||
window.Alpine.store('installerAutoAdvance', {
|
||||
active: false,
|
||||
paused: false,
|
||||
remaining: 0,
|
||||
timer: null,
|
||||
|
||||
start(seconds) {
|
||||
if (this.active) {
|
||||
return
|
||||
}
|
||||
|
||||
this.active = true
|
||||
this.paused = false
|
||||
this.remaining = seconds
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
if (this.paused) {
|
||||
return
|
||||
}
|
||||
|
||||
this.remaining--
|
||||
|
||||
if (this.remaining <= 0) {
|
||||
this.advance()
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
document
|
||||
.querySelector('.fi-sc-wizard-footer')
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
})
|
||||
},
|
||||
|
||||
toggle() {
|
||||
this.paused = ! this.paused
|
||||
},
|
||||
|
||||
advance() {
|
||||
this.stop()
|
||||
|
||||
document.querySelector('[data-installer-next]')?.click()
|
||||
},
|
||||
|
||||
stop() {
|
||||
clearInterval(this.timer)
|
||||
|
||||
this.timer = null
|
||||
this.active = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener('alpine:init', register)
|
||||
|
||||
if (window.Alpine) {
|
||||
register()
|
||||
}
|
||||
|
||||
window.addEventListener('installer-migrations-complete', (event) => {
|
||||
store()?.start(event.detail?.seconds ?? {{ $seconds }})
|
||||
})
|
||||
|
||||
// Covers both the countdown firing and the user clicking Next early, so
|
||||
// the paused countdown can't linger into the following step.
|
||||
window.addEventListener('next-wizard-step', () => store()?.stop())
|
||||
})()
|
||||
</script>
|
||||
30
tests/Feature/Installer/MigrationAutoAdvanceTest.php
Normal file
30
tests/Feature/Installer/MigrationAutoAdvanceTest.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\System\Installer;
|
||||
use Livewire\Livewire;
|
||||
|
||||
/**
|
||||
* The countdown hangs off the wizard footer that Filament renders for us, so
|
||||
* these assertions are really about the seam with the vendor component: the
|
||||
* Next button has to stay findable from JS, and the Back slot has to have been
|
||||
* taken over by Pause.
|
||||
*/
|
||||
it('renders the auto-advance countdown hooks on the wizard footer', function (): void {
|
||||
Livewire::test(Installer::class)
|
||||
->assertSee('data-installer-next', escape: false)
|
||||
->assertSee("Alpine.store('installerAutoAdvance'", escape: false)
|
||||
->assertSee('$store.installerAutoAdvance.toggle()', escape: false);
|
||||
});
|
||||
|
||||
it('replaces the wizard back button with the pause control', function (): void {
|
||||
Livewire::test(Installer::class)
|
||||
->assertSee('Pause')
|
||||
->assertDontSee(__('filament-schemas::components.wizard.actions.previous_step.label'));
|
||||
});
|
||||
|
||||
// Deliberately not covered here: calling runMigrations(). It shells out to
|
||||
// `php artisan migrate` through StreamedCommandsService, and the subprocess
|
||||
// reads .env rather than phpunit's forced sqlite connection -- so the assertion
|
||||
// would run migrations and DatabaseSeeder against the development database.
|
||||
Loading…
Reference in New Issue
Block a user