Skip to content
This repository was archived by the owner on Nov 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/Http/Controllers/Api/TagController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Http\Resources\TagResource;
use App\Models\Tag;
use App\Models\User;
use App\Rules\ValidHexColour;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
Expand Down Expand Up @@ -157,6 +158,7 @@ private function validateTag(Request $request, bool $isUpdate = false): array
$rules = [
'label' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'colour' => ['nullable', 'string', new ValidHexColour],
];

if ($isUpdate) {
Expand Down
1 change: 1 addition & 0 deletions app/Http/Resources/TagResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public function toArray(Request $request): array
'user_id' => $this->resource->user_id,
'label' => $this->resource->label,
'description' => $this->resource->description,
'colour' => $this->resource->colour,
'created_at' => $this->resource->created_at,
'updated_at' => $this->resource->updated_at,
];
Expand Down
6 changes: 6 additions & 0 deletions app/Livewire/Tags/CreateForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Livewire\Tags;

use App\Models\Tag;
use App\Rules\ValidHexColour;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
Expand All @@ -26,6 +27,9 @@ class CreateForm extends Component
/** @var string|null The optional description for the new tag. */
public ?string $description = null;

/** @var string|null The optional colour identifier for the tag. */
public ?string $colour = null;

/**
* Handle the form submission for creating a new tag.
*
Expand All @@ -36,6 +40,7 @@ public function submit(): RedirectResponse|Redirector
$this->validate([
'label' => ['required', 'string'],
'description' => ['nullable', 'string'],
'colour' => ['nullable', 'string', new ValidHexColour],
], [
'label.required' => __('Please enter a label.'),
]);
Expand All @@ -44,6 +49,7 @@ public function submit(): RedirectResponse|Redirector
'user_id' => Auth::id(),
'label' => $this->label,
'description' => $this->description ?? null,
'colour' => $this->colour,
]);

Toaster::success('The tag :label has been added.', ['label' => $tag->label]);
Expand Down
7 changes: 7 additions & 0 deletions app/Livewire/Tags/UpdateForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Livewire\Tags;

use App\Models\Tag;
use App\Rules\ValidHexColour;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
Expand All @@ -25,6 +26,9 @@ class UpdateForm extends Component
/** @var string|null The updated description for the tag. */
public ?string $description = null;

/** @var string|null An optional updated colour identifier. */
public ?string $colour = null;

/** @var Tag The tag instance being updated. */
public Tag $tag;

Expand All @@ -38,6 +42,7 @@ public function mount(Tag $tag): void
$this->tag = $tag;
$this->label = $tag->getAttribute('label');
$this->description = $tag->getAttribute('description') ?? null;
$this->colour = $tag->getAttribute('colour');
}

/**
Expand All @@ -52,13 +57,15 @@ public function submit(): RedirectResponse|Redirector
$this->validate([
'label' => ['required', 'string'],
'description' => ['nullable', 'string'],
'colour' => ['nullable', 'string', new ValidHexColour],
], [
'label.required' => __('Please enter a label.'),
]);

$this->tag->update([
'label' => $this->label,
'description' => $this->description ?? null,
'colour' => $this->colour ?? null,
]);

$this->tag->save();
Expand Down
33 changes: 33 additions & 0 deletions app/Rules/ValidHexColour.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Override;

class ValidHexColour implements ValidationRule
{
/**
* Determine if the validation rule passes.
*/
#[Override]
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value)) {
$fail('The :attribute must be a string.');

return;
}

$value = trim($value);

$pattern = '/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/';

if (! preg_match($pattern, $value)) {
$fail('The :attribute must be a valid hex color code (e.g., #d37445 or #f60).');
}
}
}
1 change: 1 addition & 0 deletions database/factories/TagFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public function definition(): array
return [
'label' => fake()->unique()->word(),
'description' => fake()->sentence(),
'colour' => fake()->hexColor(),
'user_id' => User::factory()->create()->id,
];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::table('tags', function (Blueprint $table) {
$table->string('colour')->nullable();
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,16 @@ class="mt-1 block w-full"
<div class="mt-4">
<x-input-label for="tags" :value="__('Tags')" />
@foreach ($availableTags as $tag)
<x-checkbox
id="tag-{{ $tag->id }}"
wire:model="selectedTags"
value="{{ $tag->id }}"
name="tags[]"
label="{{ $tag->label }}"
></x-checkbox>
<div class="flex items-center">
<div class="h-4 w-4 rounded mr-2 shrink-0" style="background-color: {{ $tag->colour }};"></div>
<x-checkbox
id="tag-{{ $tag->id }}"
wire:model="selectedTags"
value="{{ $tag->id }}"
name="tags[]"
label="{{ $tag->label }}"
></x-checkbox>
</div>
@endforeach

<x-input-error :messages="$errors->get('selectedTags')" class="mt-2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,13 +292,19 @@ class="text-sm text-gray-600 underline ease-in-out hover:text-gray-900 dark:text
<div class="mt-4">
<x-input-label for="tags" :value="__('Tags')" />
@foreach ($availableTags as $tag)
<x-checkbox
id="tag-{{ $tag->id }}"
wire:model="selectedTags"
value="{{ $tag->id }}"
name="tags[]"
label="{{ $tag->label }}"
></x-checkbox>
<div class="flex items-center">
<div
class="mr-2 h-4 w-4 shrink-0 rounded"
style="background-color: {{ $tag->colour }}"
></div>
<x-checkbox
id="tag-{{ $tag->id }}"
wire:model="selectedTags"
value="{{ $tag->id }}"
name="tags[]"
label="{{ $tag->label }}"
></x-checkbox>
</div>
@endforeach

<x-input-error :messages="$errors->get('selectedTags')" class="mt-2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,10 @@ class="inline-flex items-center rounded-md bg-white px-2.5 py-0.5 text-xs font-m
<div class="max-h-96 space-y-4 overflow-y-auto">
@forelse ($backupTask->tags as $tag)
<div class="flex items-center justify-between rounded-lg bg-gray-100 p-3 dark:bg-gray-700">
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $tag->label }}</span>
<div class="flex items-center">
<div class="h-4 w-4 rounded mr-2 shrink-0" style="background-color: {{ $tag->colour }};"></div>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $tag->label }}</span>
</div>
</div>
@empty
<div class="py-8 text-center text-gray-500 dark:text-gray-400">
Expand Down
8 changes: 8 additions & 0 deletions resources/views/livewire/tags/create-form.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ class="mt-1 block w-full"
></x-textarea>
<x-input-error :messages="$errors->get('description')" class="mt-2" />
</div>
<div class="mt-4">
<x-input-label for="colour" :value="__('Colour')" />
<x-text-input id="colour" class="mt-1 block w-full" type="color" wire:model="colour" name="colour" />
<x-input-error :messages="$errors->get('colour')" class="mt-2" />
<x-input-explain>
{{ __('Optionally choose a colour to easily identify your tag.') }}
</x-input-explain>
</div>
<div class="mx-auto mt-6 max-w-3xl">
<div class="flex flex-col space-y-4 sm:flex-row sm:space-x-5 sm:space-y-0">
<div class="w-full sm:w-4/6">
Expand Down
1 change: 1 addition & 0 deletions resources/views/livewire/tags/index-item.blade.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<div>
<x-table.table-row>
<div class="col-span-12 flex flex-col sm:col-span-3 sm:flex-row sm:items-center">
<div class="mr-2 h-4 w-4 shrink-0 rounded" style="background-color: {{ $tag->colour }}"></div>
<p class="font-medium text-gray-900 dark:text-gray-100">
{{ $tag->label }}
</p>
Expand Down
8 changes: 8 additions & 0 deletions resources/views/livewire/tags/update-form.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ class="mt-1 block w-full"
></x-textarea>
<x-input-error :messages="$errors->get('description')" class="mt-2" />
</div>
<div class="mt-4">
<x-input-label for="colour" :value="__('Colour')" />
<x-text-input id="colour" class="mt-1 block w-full" type="color" wire:model="colour" name="colour" />
<x-input-error :messages="$errors->get('colour')" class="mt-2" />
<x-input-explain>
{{ __('Optionally choose a colour to easily identify your tag.') }}
</x-input-explain>
</div>
<div class="mx-auto mt-6 max-w-3xl">
<div class="flex flex-col space-y-4 sm:flex-row sm:space-x-5 sm:space-y-0">
<div class="w-full sm:w-4/6">
Expand Down
4 changes: 4 additions & 0 deletions tests/Feature/Tags/Api/TagAPITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

$tagData = [
'label' => 'New Tag',
'colour' => '#d37445',
'description' => 'This is a new tag',
];

Expand All @@ -56,6 +57,7 @@
$response = $this->postJson('/api/tags', [
'label' => 'New Tag',
'description' => 'This is a new tag',
'colour' => '#d37445',
]);

$response->assertStatus(403);
Expand All @@ -73,6 +75,7 @@
'id' => $tag->id,
'label' => $tag->label,
'description' => $tag->description,
'colour' => $tag->colour,
]);
});

Expand All @@ -94,6 +97,7 @@
$updatedData = [
'label' => 'Updated Tag',
'description' => 'This is an updated tag',
'colour' => '#d37445',
];

$response = $this->putJson("/api/tags/{$tag->id}", $updatedData);
Expand Down
16 changes: 16 additions & 0 deletions tests/Feature/Tags/Livewire/CreateFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
->test('tags.create-form')
->set('label', 'New Tag')
->set('description', 'This is a new tag')
->set('colour', '#99ccff')
->call('submit');

$this->assertDatabaseHas('tags', [
'label' => 'New Tag',
'description' => 'This is a new tag',
'colour' => '#99ccff',
'user_id' => $user->id,
]);

Expand All @@ -42,3 +44,17 @@

$this->assertDatabaseCount('tags', 0);
});

test('a colour must be a valid colour', function (): void {

$user = User::factory()->create();

Livewire::actingAs($user)
->test('tags.create-form')
->set('label', 'My first tag!')
->set('colour', '#123-not-a-valid-colour')
->call('submit')
->assertHasErrors(['colour']);

$this->assertDatabaseCount('tags', 0);
});
19 changes: 17 additions & 2 deletions tests/Feature/Tags/Livewire/UpdateFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@

$user = User::factory()->create();

$tag = Tag::factory()->create(['user_id' => $user->id, 'label' => 'Old Tag', 'description' => 'Old Description']);
$tag = Tag::factory()->create(['user_id' => $user->id, 'label' => 'Old Tag', 'description' => 'Old Description', 'colour' => '#c1f11e']);

$livewire = Livewire::actingAs($user)
->test('tags.update-form', ['tag' => $tag])
->set('label', 'New Tag')
->set('description', 'New Description')
->set('colour', '#2f4de5')
->call('submit');

$this->assertDatabaseHas('tags', [
'label' => 'New Tag',
'description' => 'New Description',
'colour' => '#2f4de5',
'user_id' => $user->id,
]);

Expand All @@ -40,18 +42,20 @@
$userOne = User::factory()->create();
$userTwo = User::factory()->create();

$tag = Tag::factory()->create(['user_id' => $userOne->id, 'label' => 'Old Tag', 'description' => 'Old Description']);
$tag = Tag::factory()->create(['user_id' => $userOne->id, 'label' => 'Old Tag', 'description' => 'Old Description', 'colour' => '#c1f11e']);

$livewire = Livewire::actingAs($userTwo)
->test('tags.update-form', ['tag' => $tag])
->set('label', 'New Tag')
->set('description', 'New Description')
->set('colour', '#2f4de5')
->call('submit')
->assertForbidden();

$this->assertDatabaseHas('tags', [
'label' => 'Old Tag',
'description' => 'Old Description',
'colour' => '#c1f11e',
'user_id' => $userOne->id,
]);
});
Expand All @@ -73,3 +77,14 @@
'user_id' => $user->id,
]);
});

test('a valid hex colour is required', function (): void {

$user = User::factory()->create();
$tag = Tag::factory()->create(['user_id' => $user->id]);
$livewire = Livewire::actingAs($user)
->test('tags.update-form', ['tag' => $tag])
->set('colour', '#56756753673576')
->call('submit')
->assertHasErrors(['colour']);
});
Loading