feat(domains): edit compose redirect direction in domain modal

This commit is contained in:
Andras Bacsai
2026-08-10 15:15:48 +02:00
parent 69ba001e21
commit be2a6d6d1e
4 changed files with 76 additions and 35 deletions
+17 -1
View File
@@ -16,6 +16,8 @@ class Domains extends Component
use AuthorizesRequests;
use InteractsWithCloudflareDomainConnect;
protected bool $notifyRedirectUpdate = true;
public Application $application;
public string $redirect = 'both';
@@ -41,6 +43,8 @@ class Domains extends Component
public string $editingIndexing = 'index';
public string $editingDirection = 'both';
public ?string $editingService = null;
/** @var array<int, array{url: string, service: ?string, dns_status: string, dns_message: string, expected_ip: ?string, checked_at?: ?string, is_suggested?: bool, suggested_for?: ?string, suggestion_label?: ?string, needs_force_add?: bool}> */
@@ -100,6 +104,7 @@ class Domains extends Component
'newDomain' => ValidationPatterns::applicationDomainRules(),
'editingDomain' => ValidationPatterns::applicationDomainRules(),
'editingIndexing' => 'string|in:index,noindex',
'editingDirection' => 'string|in:both,www,non-www',
'redirect' => 'string|required|in:both,www,non-www',
'serviceRedirects' => 'array',
'serviceRedirects.*' => 'string|in:both,www,non-www',
@@ -907,6 +912,7 @@ class Domains extends Component
$this->editingIndex = $index;
$this->editingDomain = $this->domainRows[$index]['url'];
$this->editingService = $this->domainRows[$index]['service'];
$this->editingDirection = $this->serviceRedirectFor($this->editingService);
$this->editingIndexing = $this->application->isDomainNoindexed($this->editingDomain) ? 'noindex' : 'index';
$this->resetEditDomainDnsGate();
$this->resetErrorBag('editingDomain');
@@ -990,6 +996,7 @@ class Domains extends Component
$this->editingIndex = null;
$this->editingDomain = '';
$this->editingService = null;
$this->editingDirection = 'both';
$this->editingIndexing = 'index';
$this->resetEditDomainDnsGate();
$this->resetErrorBag('editingDomain');
@@ -1066,6 +1073,13 @@ class Domains extends Component
$this->application->save();
$this->resetDefaultLabels();
if ($this->isCompose && filled($service) && $this->editingDirection !== $this->savedRedirectForService($service)) {
$this->serviceRedirects[$this->serviceRedirectWireKey($service)] = $this->editingDirection;
$this->notifyRedirectUpdate = false;
$this->setServiceRedirect($service);
$this->notifyRedirectUpdate = true;
}
$this->forceSaveDomains = false;
$this->pendingAction = null;
$this->cancelEdit();
@@ -1303,7 +1317,9 @@ class Domains extends Component
$this->pendingRedirectService = null;
$this->forceSaveDomains = false;
$this->resetDefaultLabels();
$this->dispatch('success', "Redirect updated for {$serviceName}.");
if ($this->notifyRedirectUpdate) {
$this->dispatch('success', "Redirect updated for {$serviceName}.");
}
$this->refreshDomains();
$this->checkUrlsDns($addedDomains, $serviceName);
$this->pruneDomainDnsStatusesToCurrentDomains();
@@ -18,12 +18,14 @@
localEditingIndex: @js($editingIndex),
localEditingDomain: @js($editingDomain),
localEditingService: @js($editingService),
localDirection: 'both',
localIndexing: 'index',
openEditDomain(index, url, service, indexing) {
openEditDomain(index, url, service, indexing, direction) {
this.localEditingIndex = index;
this.localEditingDomain = url;
this.localEditingService = service;
this.editingServiceLabel = service || '';
this.localDirection = direction || 'both';
this.localIndexing = indexing || 'index';
this.modalOpen = true;
this.$nextTick(() => document.getElementById('editingDomainLocal')?.focus?.());
@@ -40,6 +42,7 @@
$wire.editingIndex = this.localEditingIndex;
$wire.editingDomain = this.localEditingDomain;
$wire.editingService = this.localEditingService;
$wire.editingDirection = this.localDirection;
$wire.editingIndexing = this.localIndexing;
$wire.showEditDomainModal = true;
},
@@ -50,7 +53,7 @@
return values.some((value) => this.matchesDomainSearch(value));
},
}"
@open-edit-domain.window="openEditDomain($event.detail.index, $event.detail.url, $event.detail.service, $event.detail.indexing)"
@open-edit-domain.window="openEditDomain($event.detail.index, $event.detail.url, $event.detail.service, $event.detail.indexing, $event.detail.direction)"
@edit-domain-saved.window="closeEditDomain()">
<x-application.settings-section id="domains-section" title="Domains" :helper="$helperText">
@can('update', $application)
@@ -110,10 +113,6 @@
@endcan
</div>
@endif
@elseif (! $labelsAreWritable && count($composeServices) > 0 && $composeDomainGroups->isNotEmpty())
<p class="text-sm text-neutral-500 dark:text-fg-dim">
Per-service www/non-www redirects are available next to each service group below.
</p>
@endif
</x-application.settings-section>
@@ -231,44 +230,30 @@
->map(fn ($serviceName) => $serviceName.' '.$grouped->get($serviceName, collect())->pluck('url')->implode(' '))
->values();
@endphp
<div class="overflow-hidden">
<div>
@foreach ($serviceOrder as $serviceName)
@php
$rows = $grouped->get($serviceName, collect());
$redirectWireKey = $this->serviceRedirectWireKey($serviceName);
$redirectLabel = match ($serviceRedirects[$redirectWireKey] ?? 'both') {
'www' => 'Redirect to www',
'non-www' => 'Redirect to non-www',
default => 'Allow both',
};
@endphp
<section id="application-compose-domain-group-{{ $redirectWireKey }}"
wire:key="application-compose-domain-group-{{ $redirectWireKey }}"
x-show="matchesDomainSearch(@js($serviceName.' '.$rows->pluck('url')->implode(' ')))"
class="border-b border-neutral-200 last:border-b-0 dark:border-white/10">
<div class="flex w-full items-center gap-3 px-4 py-3">
<div class="flex w-full items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-3 dark:border-white/10 dark:bg-white/[0.04]">
<span class="min-w-0 flex-1 truncate text-sm font-medium text-black dark:text-white">
{{ $serviceName }}
</span>
@unless ($labelsAreWritable)
@can('update', $application)
<div class="w-52 shrink-0">
<x-forms.listbox id="serviceRedirects.{{ $redirectWireKey }}"
htmlId="application-compose-domain-redirect-{{ $redirectWireKey }}"
live :options="[
['value' => 'both', 'label' => 'Allow www & non-www'],
['value' => 'www', 'label' => 'Redirect to www'],
['value' => 'non-www', 'label' => 'Redirect to non-www'],
]" wire:change="setServiceRedirect(@js($serviceName))" />
</div>
@else
<span class="shrink-0 text-sm text-neutral-600 dark:text-fg-dim">{{ $redirectLabel }}</span>
@endcan
@endunless
</div>
<div wire:key="application-compose-domain-rows-{{ $redirectWireKey }}-{{ md5(serialize($rows->all())) }}"
class="data-table w-full">
<div class="data-table-header domains-table-grid">
<span>Domain</span>
<span>DNS</span>
<span>Last checked</span>
<span></span>
</div>
@foreach ($rows as $row)
@php
$index = collect($domainRows)->search(
@@ -283,6 +268,7 @@
'application' => $application,
'labelsAreWritable' => $labelsAreWritable,
'isCompose' => false,
'domainDirection' => $serviceRedirects[$redirectWireKey] ?? 'both',
])
@endforeach
</div>
@@ -372,11 +358,21 @@
</div>
@unless ($labelsAreWritable)
<x-forms.listbox id="edit-domain-indexing" label="Search engine indexing"
:wire="false" value="index" x-model="localIndexing" portal :options="[
['value' => 'index', 'label' => 'Indexable'],
['value' => 'noindex', 'label' => 'Noindex'],
]" />
<div class="grid gap-4 {{ $isCompose ? 'sm:grid-cols-2' : '' }}">
@if ($isCompose)
<x-forms.listbox id="edit-domain-direction" label="Direction"
:wire="false" value="both" x-model="localDirection" portal :options="[
['value' => 'both', 'label' => 'Allow www & non-www'],
['value' => 'www', 'label' => 'Redirect to www'],
['value' => 'non-www', 'label' => 'Redirect to non-www'],
]" />
@endif
<x-forms.listbox id="edit-domain-indexing" label="Search engine indexing"
:wire="false" value="index" x-model="localIndexing" portal :options="[
['value' => 'index', 'label' => 'Indexable'],
['value' => 'noindex', 'label' => 'Noindex'],
]" />
</div>
@endunless
@if ($editDomainDnsFailed)
@@ -108,6 +108,7 @@
url: @js($row['url']),
service: @js($row['service'] ?? null),
indexing: @js($application->isDomainNoindexed($row['url']) ? 'noindex' : 'index'),
direction: @js($domainDirection ?? $redirect),
})"
class="icon-button shrink-0"
title="Edit domain" aria-label="Edit domain">
+29 -1
View File
@@ -1219,11 +1219,39 @@ it('uses the compact service domains layout for compose applications', function
expect($view)
->toContain('application-compose-domain-group-{{ $redirectWireKey }}')
->toContain('class="application-settings-section-body mt-1 scroll-mt-28')
->toContain('htmlId="application-compose-domain-redirect-{{ $redirectWireKey }}"')
->toContain('bg-neutral-50 px-4 py-3 dark:border-white/10 dark:bg-white/[0.04]')
->toContain('class="data-table-header domains-table-grid"')
->toContain('id="edit-domain-direction"')
->not->toContain('htmlId="application-compose-domain-redirect-{{ $redirectWireKey }}"')
->not->toContain('aria-label="Redirect direction for {{ $serviceName }}"')
->not->toContain('title="No domains for this service"');
});
it('updates a compose service redirect from the edit domain modal', function () {
$this->application->update([
'build_pack' => 'dockercompose',
'fqdn' => null,
'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n",
'docker_compose_domains' => json_encode([
'web' => ['domain' => 'https://web.example.com', 'redirect' => 'both'],
]),
]);
Livewire::test(Domains::class, ['application' => $this->application->fresh()])
->set('isCompose', true)
->set('composeServices', ['web'])
->call('startEdit', 0)
->assertSet('editingDirection', 'both')
->set('editingDirection', 'www')
->call('updateDomain')
->assertDispatched('success');
$domains = json_decode($this->application->fresh()->docker_compose_domains, true);
expect(data_get($domains, 'web.redirect'))->toBe('www')
->and(data_get($domains, 'web.domain'))->toContain('https://www.web.example.com');
});
it('provides client-side search for compose service domains', function () {
$view = file_get_contents(resource_path('views/livewire/project/application/domains.blade.php'));