fix: apply frontend path normalization to general settings page

Apply the same frontend path normalization pattern from commit f6398f7cf
to the General Settings page for consistency across all forms.

Changes:
- Add Alpine.js path normalization to Docker Compose section (base directory + compose location)
- Add Alpine.js path normalization to non-Docker Compose section (base directory + dockerfile location)
- Change wire:model to wire:model.defer to prevent backend requests during tab navigation
- Add @blur event handlers for immediate path normalization feedback
- Backend normalization remains as defensive fallback

This ensures consistent validation behavior and fixes potential tab focus
issues on the General Settings page.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-12-02 13:37:41 +01:00
parent 981fc127b5
commit 8714d9bd03
2 changed files with 64 additions and 26 deletions

View file

@ -606,13 +606,6 @@ public function generateDomain(string $serviceName)
}
}
public function updatedBaseDirectory()
{
if ($this->buildPack === 'dockercompose') {
$this->loadComposeFile();
}
}
public function updatedIsStatic($value)
{
if ($value) {
@ -791,6 +784,7 @@ public function submit($showToaster = true)
$oldPortsExposes = $this->application->ports_exposes;
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
$oldDockerComposeLocation = $this->initialDockerComposeLocation;
$oldBaseDirectory = $this->application->base_directory;
// Process FQDN with intermediate variable to avoid Collection/string confusion
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
@ -821,6 +815,16 @@ public function submit($showToaster = true)
return; // Stop if there are conflicts and user hasn't confirmed
}
// Normalize paths BEFORE validation
if ($this->baseDirectory && $this->baseDirectory !== '/') {
$this->baseDirectory = rtrim($this->baseDirectory, '/');
$this->application->base_directory = $this->baseDirectory;
}
if ($this->publishDirectory && $this->publishDirectory !== '/') {
$this->publishDirectory = rtrim($this->publishDirectory, '/');
$this->application->publish_directory = $this->publishDirectory;
}
$this->application->save();
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
@ -828,7 +832,10 @@ public function submit($showToaster = true)
$this->application->save();
}
if ($this->buildPack === 'dockercompose' && $oldDockerComposeLocation !== $this->dockerComposeLocation) {
// Validate docker compose file path when base directory OR compose location changes
if ($this->buildPack === 'dockercompose' &&
($oldDockerComposeLocation !== $this->dockerComposeLocation ||
$oldBaseDirectory !== $this->baseDirectory)) {
$compose_return = $this->loadComposeFile(showToast: false);
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
return;
@ -855,14 +862,6 @@ public function submit($showToaster = true)
$this->application->ports_exposes = $port;
}
}
if ($this->baseDirectory && $this->baseDirectory !== '/') {
$this->baseDirectory = rtrim($this->baseDirectory, '/');
$this->application->base_directory = $this->baseDirectory;
}
if ($this->publishDirectory && $this->publishDirectory !== '/') {
$this->publishDirectory = rtrim($this->publishDirectory, '/');
$this->application->publish_directory = $this->publishDirectory;
}
if ($this->buildPack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
if ($this->application->isDirty('docker_compose_domains')) {

View file

@ -241,12 +241,32 @@
@else
<div class="flex flex-col gap-2">
@endcan
<div class="flex gap-2">
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/" id="baseDirectory"
label="Base Directory" helper="Directory to use as root. Useful for monorepos." />
<div x-data="{
baseDir: '{{ $application->base_directory }}',
composeLocation: '{{ $application->docker_compose_location }}',
normalizePath(path) {
if (!path || path.trim() === '') return '/';
path = path.trim();
path = path.replace(/\/+$/, '');
if (!path.startsWith('/')) {
path = '/' + path;
}
return path;
},
normalizeBaseDir() {
this.baseDir = this.normalizePath(this.baseDir);
},
normalizeComposeLocation() {
this.composeLocation = this.normalizePath(this.composeLocation);
}
}" class="flex gap-2">
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/" wire:model.defer="baseDirectory"
label="Base Directory" helper="Directory to use as root. Useful for monorepos."
x-model="baseDir" @blur="normalizeBaseDir()" />
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/docker-compose.yaml"
id="dockerComposeLocation" label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
wire:model.defer="dockerComposeLocation" label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>"
x-model="composeLocation" @blur="normalizeComposeLocation()" />
</div>
<div class="w-96">
<x-forms.checkbox instantSave id="isPreserveRepositoryEnabled"
@ -293,13 +313,32 @@
@endif
</div>
@else
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="baseDirectory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." x-bind:disabled="!canUpdate" />
<div x-data="{
baseDir: '{{ $application->base_directory }}',
dockerfileLocation: '{{ $application->dockerfile_location }}',
normalizePath(path) {
if (!path || path.trim() === '') return '/';
path = path.trim();
path = path.replace(/\/+$/, '');
if (!path.startsWith('/')) {
path = '/' + path;
}
return path;
},
normalizeBaseDir() {
this.baseDir = this.normalizePath(this.baseDir);
},
normalizeDockerfileLocation() {
this.dockerfileLocation = this.normalizePath(this.dockerfileLocation);
}
}" class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" wire:model.defer="baseDirectory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." x-bind:disabled="!canUpdate"
x-model="baseDir" @blur="normalizeBaseDir()" />
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
<x-forms.input placeholder="/Dockerfile" id="dockerfileLocation" label="Dockerfile Location"
<x-forms.input placeholder="/Dockerfile" wire:model.defer="dockerfileLocation" label="Dockerfile Location"
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>"
x-bind:disabled="!canUpdate" />
x-bind:disabled="!canUpdate" x-model="dockerfileLocation" @blur="normalizeDockerfileLocation()" />
@endif
@if ($application->build_pack === 'dockerfile')