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:
Andras Bacsai
2026-09-24 08:26:45 +02:00
co-authored by Claude Opus 5.5
parent 922440447b
commit d1b30ce704
5 changed files with 126 additions and 6 deletions
+4
View File
@@ -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()`.
+5 -5
View File
@@ -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>
+81
View File
@@ -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();
});
+35
View File
@@ -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');
});