Source Path
Destination Path
@if ($supportsPreviewSuffix)
-
- PR suffix
-
+
+ PR suffix
+
+
@endif
Backup
@if ($showActionsColumn)
@@ -74,7 +74,10 @@ class="block min-w-0 truncate text-[13px] text-neutral-950 dark:text-fg"
@if ($supportsPreviewSuffix)
-
PR suffix
+
+ PR suffix
+
+
{{ $form['isPreviewSuffixEnabled'] ? 'Add suffix' : 'Share volume' }}
@endif
@@ -82,16 +85,22 @@ class="block min-w-0 truncate text-[13px] text-neutral-950 dark:text-fg"
+
+ ]" canGate="update" :canResource="$resource" />
Backup
@if ($hasEnabledBackup)
-
-
-
-
$hasS3Backup])
- title="{{ $hasS3Backup ? 'Backups are saved to S3' : 'Backups are stored locally only' }}">
- {{ $hasS3Backup ? 'S3' : 'Local' }}
-
+ @if ($backupUrl)
+
$hasS3Backup,
+ ])
+ title="Volume backup is enabled"
+ aria-label="{{ $hasS3Backup ? 'Backups are saved to S3' : 'Backups are stored locally only' }}">
+ {{ $hasS3Backup ? 'S3' : 'Local' }}
+
+ @else
+
$hasS3Backup])
+ title="Volume backup is enabled"
+ aria-label="{{ $hasS3Backup ? 'Backups are saved to S3' : 'Backups are stored locally only' }}">
+ {{ $hasS3Backup ? 'S3' : 'Local' }}
+
+ @endif
@else
-
@endif
@@ -200,10 +218,10 @@ class="volumes-col-actions volumes-cell-actions flex flex-wrap items-center just
@if ($showBackupAction)
-
+
+ Backup
+
@if ($resource instanceof \App\Models\Application)
-
+
+ Backup
+
diff --git a/tests/Feature/ListboxTriggerTruncationTest.php b/tests/Feature/ListboxTriggerTruncationTest.php
index 82e526bb3..a7cd46295 100644
--- a/tests/Feature/ListboxTriggerTruncationTest.php
+++ b/tests/Feature/ListboxTriggerTruncationTest.php
@@ -78,6 +78,13 @@
->toContain('Math.max(triggerRect.width, panel.offsetWidth)');
});
+test('portaled listboxes stay anchored when a scroll container moves', function () {
+ $listbox = file_get_contents(resource_path('views/components/forms/listbox.blade.php'));
+
+ expect($listbox)
+ ->toContain('@scroll.window.capture="open && positionPanel()"');
+});
+
test('searchable listbox component uses shared trigger label truncation', function () {
$html = Blade::render(<<<'BLADE'
toContain('12rem')
->not->toContain('17.5rem');
+ expect($allView)
+ ->toContain("'table-badge', 'table-badge-success' => \$hasS3Backup")
+ ->not->toContain('');
+
+ expect($css)->toMatch('/@media \(max-width: 768px\)[\s\S]*?\.volumes-col-backup\s*\{[^}]*flex-direction:\s*row;[^}]*align-items:\s*center;/');
+
// Settings form labels are 13px (not Tailwind text-sm 14px).
expect($css)
->toMatch('/\.application-settings-form label\s*\{[^}]*font-size:\s*13px/s');
});
+it('renders volume actions and PR suffix controls as valid markup', function () {
+ [$application] = createApplicationWithVolume();
+ LocalPersistentVolume::create([
+ 'uuid' => (string) Str::uuid(),
+ 'name' => $application->uuid.'-cache',
+ 'mount_path' => '/cache',
+ 'resource_id' => $application->id,
+ 'resource_type' => $application->getMorphClass(),
+ 'is_preview_suffix_enabled' => true,
+ ]);
+
+ $html = Livewire::test(All::class, ['resource' => $application])->html();
+ $document = new DOMDocument;
+ $previousState = libxml_use_internal_errors(true);
+ $loaded = $document->loadHTML($html);
+ libxml_clear_errors();
+ libxml_use_internal_errors($previousState);
+ $xpath = new DOMXPath($document);
+ $helperText = 'Adds -pr-N to the storage name or path so each preview uses isolated data. Disabling it shares production data with previews.';
+
+ expect($loaded)->toBeTrue()
+ ->and($xpath->query("//*[contains(concat(' ', normalize-space(@class), ' '), ' volumes-col-actions ')]//button[normalize-space(.)='Backup']"))->toHaveCount(2)
+ ->and($xpath->query("//*[contains(concat(' ', normalize-space(@class), ' '), ' volumes-col-actions ')]//button[normalize-space(.)='Backup']//svg"))->toHaveCount(0)
+ ->and($xpath->query("//button[@aria-label='More information']/following-sibling::*[@role='tooltip'][contains(normalize-space(.), '{$helperText}')]"))->toHaveCount(3)
+ ->and($xpath->query("//template[@x-teleport='body']/*[@role='listbox']"))->toHaveCount(2);
+});
+
+it('declares explicit authorization on the changed storage controls', function () {
+ $view = file_get_contents(resource_path('views/livewire/project/shared/storages/all.blade.php'));
+
+ preg_match(
+ '//',
+ $view,
+ $previewSuffixListbox
+ );
+
+ expect($previewSuffixListbox[0] ?? '')
+ ->toContain('canGate="update"')
+ ->toContain(':canResource="$resource"');
+
+ preg_match_all('/]*>\s*Backup\s*<\/x-forms\.button>/s', $view, $backupButtons);
+
+ expect($backupButtons[0])->toHaveCount(3);
+
+ foreach ($backupButtons[0] as $backupButton) {
+ expect($backupButton)
+ ->toContain('canGate="update"')
+ ->toContain(':canResource="$resource"');
+ }
+});
+
+it('uses valid block wrappers around PR suffix helpers', function () {
+ $view = file_get_contents(resource_path('views/livewire/project/shared/storages/all.blade.php'));
+
+ expect($view)
+ ->not->toContain('')
+ ->not->toContain('');
+
+ expect(substr_count($view, ''))
+ ->toBe(3);
+});
+
it('creates and exposes volume backups for service storage', function () {
$service = Service::factory()->create([
'environment_id' => $this->environment->id,
diff --git a/tests/v4/Browser/ApplicationConfigurationTest.php b/tests/v4/Browser/ApplicationConfigurationTest.php
index fa7fb4ffa..161f96f66 100644
--- a/tests/v4/Browser/ApplicationConfigurationTest.php
+++ b/tests/v4/Browser/ApplicationConfigurationTest.php
@@ -1,5 +1,6 @@
screenshot(filename: 'application-configuration-overview');
});
+it('keeps the PR suffix listbox visible outside the volumes table while scrolling', function () {
+ foreach (range(1, 8) as $index) {
+ LocalPersistentVolume::create([
+ 'uuid' => (string) new Cuid2,
+ 'name' => $this->application->uuid.'-data-'.$index,
+ 'mount_path' => '/data/'.$index,
+ 'resource_id' => $this->application->id,
+ 'resource_type' => $this->application->getMorphClass(),
+ 'is_preview_suffix_enabled' => true,
+ ]);
+ }
+
+ loginAndSkipBoarding();
+
+ $url = applicationConfigurationUrl(
+ $this->stack['project'],
+ $this->stack['environment'],
+ $this->application
+ ).'/persistent-storage';
+
+ $page = visit($url);
+ $page->assertSee('PR suffix')
+ ->assertSee('Add suffix')
+ ->assertScript(<<<'JS'
+ () => {
+ const table = document.querySelector('.data-table');
+ const trigger = document.querySelector('[id$="isPreviewSuffixEnabled-trigger"]');
+ window.__volumeTableScrollHeight = table.scrollHeight;
+ trigger.click();
+
+ return true;
+ }
+ JS)
+ ->wait(0.2)
+ ->assertScript(<<<'JS'
+ () => {
+ const table = document.querySelector('.data-table');
+ const panel = document.querySelector('[id$="isPreviewSuffixEnabled-panel"]');
+ const beforeScroll = panel.getBoundingClientRect();
+ window.scrollBy(0, 100);
+ const afterScroll = panel.getBoundingClientRect();
+
+ return panel.parentElement === document.body
+ && table.scrollHeight === window.__volumeTableScrollHeight
+ && window.scrollY > 0
+ && beforeScroll.top >= 0
+ && beforeScroll.bottom <= window.innerHeight
+ && afterScroll.top >= 0
+ && afterScroll.bottom <= window.innerHeight;
+ }
+ JS)
+ ->assertNoJavaScriptErrors()
+ ->screenshot(filename: 'application-persistent-storage-pr-suffix-listbox');
+});
+
it('saves application name description and ports from the general form', function () {
loginAndSkipBoarding();