fix(modal): keep confirmation checkbox state in one place

The dialog checkboxes used both Alpine selectedActions and wire:model.
After the dialog closed, the two could differ, so a click did not change
the checkbox. Alpine now holds the state, and the dialog sends it to
Livewire on submit.

Closes #12338

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-26 19:28:36 +01:00
co-authored by Claude Opus 5.5
parent 4559d21b4c
commit b17c83d688
2 changed files with 89 additions and 6 deletions
@@ -68,6 +68,7 @@
dispatchAction: @js($dispatchAction),
submitting: false,
passwordError: '',
checkboxIds: @js(collect($checkboxes)->pluck('id')->values()->all()),
selectedActions: @js(collect($checkboxes)->pluck('id')->filter(fn($id) => $this->$id)->values()->all()),
dispatchEvent: @js($dispatchEvent),
dispatchEventType: @js($dispatchEventType),
@@ -103,6 +104,9 @@
return Promise.resolve(this.passwordError);
}
}
// The checkboxes are not bound with wire:model, so send their state with the call.
// Methods that read the properties (for example stop() with $docker_cleanup) get what the user saw.
this.checkboxIds.forEach(id => $wire.$set(id, this.selectedActions.includes(id), false));
if (this.dispatchAction) {
$wire.dispatch(this.submitAction);
return Promise.resolve(true);
@@ -127,11 +131,9 @@
}
});
},
toggleAction(id) {
const index = this.selectedActions.indexOf(id);
if (index > -1) {
this.selectedActions.splice(index, 1);
} else {
setAction(id, checked) {
this.selectedActions = this.selectedActions.filter(selectedId => selectedId !== id);
if (checked) {
this.selectedActions.push(id);
}
}
@@ -245,7 +247,8 @@
@foreach ($checkboxes as $index => $checkbox)
<div class="flex justify-between items-center mb-2">
<x-forms.checkbox fullWidth :label="$checkbox['label']" :id="$checkbox['id']"
x-on:change="toggleAction('{{ $checkbox['id'] }}')" :checked="$this->{$checkbox['id']}"
:domValue="$checkbox['id']" autocomplete="off"
x-on:change="setAction('{{ $checkbox['id'] }}', $event.target.checked)" :checked="$this->{$checkbox['id']}"
x-bind:checked="selectedActions.includes('{{ $checkbox['id'] }}')" />
</div>
@endforeach
@@ -0,0 +1,80 @@
<?php
use App\Actions\Application\StopApplication;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Lorisleiva\Actions\Decorators\JobDecorator;
uses(RefreshDatabase::class);
beforeEach(function () {
config()->set('app.maintenance.store', 'array');
$this->stack = seedBrowserResourceStack();
$this->application = createBrowserApplication($this->stack, [
'uuid' => 'app-browser-stop-modal',
'name' => 'Stop Modal App',
'status' => 'running:healthy',
]);
$this->url = applicationConfigurationUrl($this->stack['project'], $this->stack['environment'], $this->application);
});
const OPEN_STOP_DIALOG = <<<'JS'
() => {
document.getElementById('application-mobile-stop-trigger').click();
return true;
}
JS;
const CLOSE_DIALOG = <<<'JS'
() => {
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
return true;
}
JS;
const CLEANUP_CHECKBOX = 'input[id^="docker_cleanup-"]';
it('toggles the checkbox even when its DOM state differs from the selected actions', function () {
loginAndSkipBoarding();
visit($this->url)
->assertSee('Stop Modal App')
->assertScript(OPEN_STOP_DIALOG)
->wait(0.5)
->assertScript('() => { const el = document.querySelector(\''.CLEANUP_CHECKBOX.'\'); el.checked = false; return Alpine.$data(el).selectedActions.includes("docker_cleanup"); }')
->click(CLEANUP_CHECKBOX)
->wait(0.3)
->assertScript('() => document.querySelector(\''.CLEANUP_CHECKBOX.'\').checked', true)
->click(CLEANUP_CHECKBOX)
->wait(0.3)
->assertScript('() => document.querySelector(\''.CLEANUP_CHECKBOX.'\').checked', false)
->screenshot(filename: 'modal-confirmation-checkbox-toggle');
});
it('stops with the cleanup option that the dialog shows after it was closed and opened again', function () {
Queue::fake();
loginAndSkipBoarding();
visit($this->url)
->assertSee('Stop Modal App')
->assertScript(OPEN_STOP_DIALOG)
->wait(0.5)
->click(CLEANUP_CHECKBOX)
->wait(0.3)
->assertScript(CLOSE_DIALOG)
->wait(1)
->assertScript(OPEN_STOP_DIALOG)
->wait(0.5)
->assertScript('() => document.querySelector(\''.CLEANUP_CHECKBOX.'\').checked', true)
->click('Continue')
->wait(0.3)
->click('Confirm')
->wait(1.5)
->screenshot(filename: 'modal-confirmation-checkbox-stop');
Queue::assertPushed(JobDecorator::class, fn (JobDecorator $job) => $job->getAction() instanceof StopApplication
&& $job->getParameters()[2] === true);
});