mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-26 01:10:30 -04:00
fix(tags): pass only the tag id when quick adding a tag
The available tags list now sends only the tag id to the Livewire action. The component resolves the tag from the current team, so tag names with special characters, such as apostrophes, work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
922440447b
commit
d1b30ce704
@@ -5,6 +5,7 @@
|
||||
- When a symptom matches an earlier fix, inspect that fix and prove why it no longer works before adding another workaround.
|
||||
- Test old reports against the current branch because later changes can make the report obsolete.
|
||||
- Use the same regression test before and after the production change so the result shows the behavior difference.
|
||||
- Call `visit()` directly in each `tests/v4/Browser` test body; Pest does not mark a test that only uses helper-wrapped `visit()` as a browser test, so it fails with `sendText() on null`.
|
||||
|
||||
## Verify the complete user flow
|
||||
- Do not use a passing unit test, a successful build, or a healthy process as proof for a reported UI failure.
|
||||
@@ -49,3 +50,6 @@
|
||||
|
||||
## Fail closed at public webhook boundaries
|
||||
- Reject missing or blank secrets before signature verification, and return generic errors without logging secrets, signatures, or payloads.
|
||||
|
||||
## Pass identities to Livewire actions
|
||||
- Pass record IDs to Livewire actions instead of display values, and resolve team-scoped records on the server. When JavaScript needs text, use `@js()` or `Js::from()`.
|
||||
|
||||
@@ -67,17 +67,17 @@ class Tags extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function addTag(string $id, string $name)
|
||||
public function addTag(string $id)
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
$name = strip_tags($name);
|
||||
if ($this->resource->tags()->where('id', $id)->exists()) {
|
||||
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='dark:text-warning'>$name</span> already added.");
|
||||
$tag = Tag::ownedByCurrentTeam()->findOrFail($id);
|
||||
if ($this->resource->tags()->whereKey($tag->id)->exists()) {
|
||||
$this->dispatch('error', 'Duplicate tags.', 'Tag <span class=\'dark:text-warning\'>'.e($tag->name).'</span> already added.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->resource->tags()->attach($id);
|
||||
$this->resource->tags()->attach($tag->id);
|
||||
$this->refresh();
|
||||
$this->dispatch('success', 'Tag added.');
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach ($filteredTags as $tag)
|
||||
<x-forms.button wire:key="available-tag-{{ $tag->id }}"
|
||||
wire:click="addTag('{{ $tag->id }}', '{{ $tag->name }}')">
|
||||
wire:click="addTag('{{ $tag->id }}')">
|
||||
<x-reicon name="plus" class="size-3.5 text-coollabs dark:text-warning" />
|
||||
{{ $tag->name }}
|
||||
</x-forms.button>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\Tags;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
$this->team = Team::factory()->create();
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->first();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
test('quick add passes only the tag id to the action', function () {
|
||||
$name = "o'reilly";
|
||||
Livewire::test(Tags::class, ['resource' => $this->application])
|
||||
->set('newTags', $name)
|
||||
->call('submit')
|
||||
->assertNotDispatched('error');
|
||||
|
||||
$tag = Tag::ownedByCurrentTeam()->where('name', $name)->firstOrFail();
|
||||
$otherApplication = Application::factory()->create([
|
||||
'environment_id' => $this->application->environment_id,
|
||||
'destination_id' => $this->application->destination_id,
|
||||
'destination_type' => $this->application->destination_type,
|
||||
]);
|
||||
|
||||
$html = Livewire::test(Tags::class, ['resource' => $otherApplication])->html();
|
||||
expect($html)->toContain("wire:click=\"addTag('{$tag->id}')\"")
|
||||
->not->toMatch('/wire:click="[^"]*reilly/');
|
||||
});
|
||||
|
||||
test('addTag accepts only a tag owned by the current team', function () {
|
||||
$ownTag = Tag::create(['name' => 'own-tag', 'team_id' => $this->team->id]);
|
||||
$otherTag = Tag::create(['name' => 'other-tag', 'team_id' => Team::factory()->create()->id]);
|
||||
|
||||
Livewire::test(Tags::class, ['resource' => $this->application])
|
||||
->call('addTag', (string) $otherTag->id);
|
||||
expect($this->application->tags()->whereKey($otherTag->id)->exists())->toBeFalse();
|
||||
|
||||
Livewire::test(Tags::class, ['resource' => $this->application])
|
||||
->call('addTag', (string) $ownTag->id);
|
||||
expect($this->application->tags()->whereKey($ownTag->id)->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot create a tag through resource settings', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Tags::class, ['resource' => $this->application])
|
||||
->set('newTags', 'member-tag')
|
||||
->call('submit')
|
||||
->assertDispatched('error');
|
||||
expect(Tag::where('name', 'member-tag')->exists())->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('quick adds an existing tag whose name contains an apostrophe', function () {
|
||||
$stack = seedBrowserResourceStack();
|
||||
$source = createBrowserApplication($stack, ['name' => 'Source App']);
|
||||
$target = createBrowserApplication($stack, ['name' => 'Target App']);
|
||||
$source->tags()->attach($tag = Tag::create(['name' => "o'reilly", 'team_id' => 0]));
|
||||
|
||||
$page = visit('/login')
|
||||
->fill('email', 'test@example.com')
|
||||
->fill('password', 'password')
|
||||
->click('Login')
|
||||
->assertSee('Dashboard')
|
||||
->navigate(route('project.application.tags', [
|
||||
'project_uuid' => $stack['project']->uuid,
|
||||
'environment_uuid' => $stack['environment']->uuid,
|
||||
'application_uuid' => $target->uuid,
|
||||
]))
|
||||
->assertSee("o'reilly");
|
||||
|
||||
$page->script(<<<'JS'
|
||||
() => [...document.querySelectorAll('#available-tags-section button')]
|
||||
.find(button => button.textContent.includes('reilly'))
|
||||
.click()
|
||||
JS);
|
||||
$page->wait(1.5);
|
||||
|
||||
expect($target->tags()->whereKey($tag->id)->exists())->toBeTrue();
|
||||
$page->screenshot(filename: 'tag-quick-add');
|
||||
});
|
||||
Reference in New Issue
Block a user