feat(previews): move pull request settings into a modal

Add pull request loading, refreshing, and deployment controls to the preview settings modal, simplify modal subtitles, and update sponsor listings.
This commit is contained in:
Andras Bacsai
2026-09-16 13:51:18 +02:00
parent ac1123ec56
commit 63f26aefd9
7 changed files with 103 additions and 75 deletions
+8
View File
@@ -57,7 +57,9 @@ Thank you so much!
### Huge Sponsors
* [CubePath](https://cubepath.com/coolify) - Premium dedicated servers and cloud VPS hosting
* [Context.dev](https://www.context.dev/) - Web scraping API for AI agents
* [Ginernet](https://ginernet.com/) - Hosting powerful servers in Spain
* [SerpAPI](https://serpapi.com) - Google Search API — Scrape Google and other search engines from our fast, easy, and complete API.
* [MVPS](https://www.mvps.net) - Cheap VPS servers at the highest possible quality
* [ScreenshotOne](https://screenshotone.com) - Screenshot API for devs
@@ -67,6 +69,7 @@ Thank you so much!
### Big Sponsors
* [Vanaways](https://www.vanaways.co.uk) - New vans for sale and lease across the UK
* [Cloudways](https://www.cloudways.com/en/?id=2125302) - Managed cloud hosting platform by DigitalOcean
* [ByteBase](https://www.bytebase.com) - Database CI/CD and Security at Scale
* [Ramnode](https://ramnode.com/) - High Performance Cloud VPS Hosting
@@ -105,6 +108,9 @@ Thank you so much!
### Small Sponsors
<a href="https://darkvps.pro"><img width="60px" alt="DarkVPS" src="https://cdn.coollabs.io/sponsors/darkvps.png"/></a>
<a href="https://www.opensourcealternatives.to"><img width="60px" alt="Open Source Alternatives" src="https://cdn.coollabs.io/sponsors/opensourcealternatives.png"/></a>
<a href="https://onserva.com/"><img width="60px" alt="Onserva" src="https://onserva.com/icon.svg"/></a>
<a href="https://www.movavi.com/mac.html?utm_source=coolify.io"><img width="60px" alt="Movavi" src="https://cdn.coollabs.io/sponsors/movavi.png"/></a>
<a href="https://usefoil.com/"><img width="60px" alt="ABXY" src="https://usefoil.com/favicon.svg"/></a>
<a href="https://www.launchfa.st/?utm_source=coolify.io"><img width="60px" alt="LaunchFast Boilerplates" src="https://github.com/LaunchFast-Boilerplates.png"/></a>
@@ -133,6 +139,8 @@ Thank you so much!
<a href="https://cirun.io/?utm_source=coolify.io"><img width="60px" alt="Cirun" src="https://cdn.coollabs.io/sponsors/cirun-logo.png"/></a>
<a href="https://github.com/puls-digital-group"><img width="60px" alt="Puls Digital Group" src="https://github.com/puls-digital-group.png"/></a>
<a href="https://github.com/jonathanprl"><img width="60px" alt="Jonathan Pereira" src="https://github.com/jonathanprl.png"/></a>
<a href="https://outboundgateway.com/"><img width="60px" alt="OutboundGateway" src="https://github.com/OutboundGateway.png"/></a>
<a href="https://github.com/t4dt"><img width="60px" alt="T4DT GmbH" src="https://github.com/t4dt.png"/></a>
<a href="https://internetgarden.co/?utm_source=coolify.io"><img width="60px" alt="Internet Garden" src="https://cdn.coollabs.io/sponsors/internetgarden.ico"/></a>
<a href="https://evercam.io/?utm_source=coolify.io"><img width="60px" alt="Evercam" src="https://github.com/evercam.png"/></a>
<a href="https://web3.career/?utm_source=coolify.io"><img width="60px" alt="Web3 Jobs" src="https://cdn.coollabs.io/sponsors/web3jobs.png"/></a>
@@ -204,9 +204,6 @@
],
'project.application.preview-deployments' => array_values(array_filter([
['id' => 'preview-template-section', 'label' => 'URL template'],
$application->is_github_based()
? ['id' => 'preview-pull-requests-section', 'label' => 'Pull requests']
: null,
$application->build_pack === 'dockerimage'
? ['id' => 'manual-preview-section', 'label' => 'Manual preview']
: null,
@@ -62,16 +62,9 @@
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
<header class="flex-wrap! sm:flex-nowrap!">
<div class="min-w-0 flex-1 py-0.5">
<h3 class="truncate">{{ $title }}</h3>
@if ($subtitle)
<h3>
<x-helper :helper="$subtitle" :label="'More information about '.$title">
<x-slot:trigger>
<span class="underline underline-offset-4">{{ $title }}</span>
</x-slot:trigger>
</x-helper>
</h3>
@else
<h3 class="truncate">{{ $title }}</h3>
<p class="mt-0.5 text-xs text-neutral-500 dark:text-fg-dim">{{ $subtitle }}</p>
@endif
</div>
@isset($headerActions)
@@ -7,6 +7,70 @@
helper="Automatic pull request deployments and who can trigger them.">
<x-slot:actions>
@can('update', $application)
@if ($application->is_github_based())
<x-modal-input title="Pull requests"
subtitle="Load open pull requests from GitHub, then configure or deploy a preview."
:wireIgnore="false" :isLarge="true">
<x-slot:content>
<x-forms.button wire:click="load_prs">
Load pull requests
</x-forms.button>
</x-slot:content>
<x-slot:headerActions>
@isset($rate_limit_remaining)
<span class="text-xs text-neutral-500 dark:text-fg-dim">
{{ $rate_limit_remaining }} requests remaining
</span>
@endisset
<x-forms.button wire:click="load_prs">
Refresh
</x-forms.button>
</x-slot:headerActions>
<div class="flex min-h-48 items-center justify-center" wire:loading wire:target="load_prs">
<x-loading text="Loading pull requests…" />
</div>
<div class="-m-4" wire:loading.remove wire:target="load_prs">
@forelse ($pull_requests as $pull_request)
<div
class="flex flex-col gap-3 border-b border-neutral-200 px-4 py-3.5 last:border-b-0 sm:flex-row sm:items-center dark:border-white/[0.07]">
<div
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-mono text-xs font-semibold text-neutral-600 ring-1 ring-neutral-200 dark:bg-white/[0.05] dark:text-fg-dim dark:ring-white/[0.07]">
#{{ data_get($pull_request, 'number') }}
</div>
<div class="min-w-0 flex-1">
<h4 class="truncate text-sm font-semibold text-black dark:text-fg">
{{ data_get($pull_request, 'title') }}
</h4>
<a target="_blank"
class="mt-1 inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-coollabs dark:text-fg-dim dark:hover:text-warning"
href="{{ data_get($pull_request, 'html_url') }}">
Open on GitHub
<x-external-link />
</a>
</div>
<div class="flex shrink-0 items-center gap-2">
<x-forms.button
wire:click="add('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
Configure
</x-forms.button>
@can('deploy', $application)
<x-forms.button
wire:click="add_and_deploy('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
Deploy preview
</x-forms.button>
@endcan
</div>
</div>
@empty
<x-empty size="sm" title="No open pull requests"
description="No open pull requests were found for this repository."
icon-name="sources" />
@endforelse
</div>
</x-modal-input>
@endif
@if ($isPreviewDeploymentsEnabled)
<x-forms.button wire:click="togglePreviewDeployments" wire:target="togglePreviewDeployments">
Disable preview deployments
@@ -39,65 +103,6 @@
</x-callout>
@endif
@if ($application->is_github_based())
<x-application.settings-section id="preview-pull-requests-section" title="Pull requests"
helper="Load open pull requests from GitHub, then configure or deploy a preview." flush>
<x-slot:actions>
@isset($rate_limit_remaining)
<span class="text-xs text-neutral-500 dark:text-fg-dim">
{{ $rate_limit_remaining }} requests remaining
</span>
@endisset
@can('update', $application)
<x-forms.button wire:click="load_prs">
Load pull requests
</x-forms.button>
@endcan
</x-slot:actions>
<div>
@forelse ($pull_requests as $pull_request)
<div
class="flex flex-col gap-3 border-b border-neutral-200 px-4 py-3.5 last:border-b-0 sm:flex-row sm:items-center dark:border-white/[0.07]">
<div
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-mono text-xs font-semibold text-neutral-600 ring-1 ring-neutral-200 dark:bg-white/[0.05] dark:text-fg-dim dark:ring-white/[0.07]">
#{{ data_get($pull_request, 'number') }}
</div>
<div class="min-w-0 flex-1">
<h4 class="truncate text-sm font-semibold text-black dark:text-fg">
{{ data_get($pull_request, 'title') }}
</h4>
<a target="_blank"
class="mt-1 inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-coollabs dark:text-fg-dim dark:hover:text-warning"
href="{{ data_get($pull_request, 'html_url') }}">
Open on GitHub
<x-external-link />
</a>
</div>
<div class="flex shrink-0 items-center gap-2">
@can('update', $application)
<x-forms.button
wire:click="add('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
Configure
</x-forms.button>
@endcan
@can('deploy', $application)
<x-forms.button
wire:click="add_and_deploy('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
Deploy preview
</x-forms.button>
@endcan
</div>
</div>
@empty
<x-empty size="sm" title="No pull requests loaded"
description="Load open pull requests from GitHub to configure a preview deployment."
icon-name="sources" />
@endforelse
</div>
</x-application.settings-section>
@endif
@if ($application->build_pack === 'dockerimage')
<x-application.settings-section id="manual-preview-section" title="Manual preview"
helper="Deploy a preview directly from a Docker image tag.">
@@ -117,6 +117,21 @@ it('renders preview deployment enablement as a section action', function () {
expect($this->application->fresh()->settings->is_preview_deployments_enabled)->toBeTrue();
});
it('renders GitHub pull requests in a modal opened from the preview settings', function () {
$view = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php'));
$sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
expect($view)
->toContain('<x-modal-input title="Pull requests"')
->toContain(':isLarge="true"')
->toContain('wire:click="load_prs"')
->not->toContain('id="preview-pull-requests-section"')
->and(strpos($view, '<x-modal-input title="Pull requests"'))
->toBeLessThan(strpos($view, '<livewire:project.application.preview.form'));
expect($sidebar)->not->toContain("['id' => 'preview-pull-requests-section', 'label' => 'Pull requests']");
});
it('does not show git preview settings for non-git applications', function (string $buildPack, ?string $dockerfile) {
$this->application->update(['build_pack' => $buildPack, 'dockerfile' => $dockerfile]);
+10
View File
@@ -17,6 +17,16 @@ test('input modal overlay is fixed to the viewport without its own page scrollba
->not->toContain('class="fixed inset-0 z-99 overflow-y-auto"');
});
test('input modal renders its subtitle as static text instead of a tooltip', function () {
$html = Blade::render('<x-modal-input title="Pull requests" subtitle="Load repository pull requests.">Modal content</x-modal-input>');
expect($html)
->toContain('Load repository pull requests.')
->toContain('text-xs text-neutral-500 dark:text-fg-dim')
->not->toContain('More information about Pull requests')
->not->toContain('role="tooltip"');
});
test('confirmation modal closes before dispatching an event that can open another modal', function () {
$modal = file_get_contents(resource_path('views/components/modal-confirmation.blade.php'));
@@ -441,14 +441,14 @@ it('centers the rollback image loading state across the card', function () {
->toContain('flex items-center justify-center');
});
it('shows pull request loading feedback only on the action button', function () {
it('shows pull request loading feedback in the modal body', function () {
$previews = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php'));
expect($previews)
->toContain('wire:click="load_prs"')
->not->toContain('wire:loading.remove wire:target="load_prs"')
->not->toContain('wire:loading wire:target="load_prs"')
->not->toContain('Loading pull requests…');
->toContain('wire:loading.remove wire:target="load_prs"')
->toContain('wire:loading wire:target="load_prs"')
->toContain('Loading pull requests…');
});
it('shows loading feedback while a service deployment starts', function () {