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 (#11971)
This commit is contained in:
@@ -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