Release v1.4.0 #146
6 changed files with 114 additions and 4 deletions
36
app/Enums/FeedColorEnum.php
Normal file
36
app/Enums/FeedColorEnum.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum FeedColorEnum: string
|
||||||
|
{
|
||||||
|
case SLATE = 'slate';
|
||||||
|
case AMBER = 'amber';
|
||||||
|
case SKY = 'sky';
|
||||||
|
case VIOLET = 'violet';
|
||||||
|
case TEAL = 'teal';
|
||||||
|
case ROSE = 'rose';
|
||||||
|
case INDIGO = 'indigo';
|
||||||
|
case ORANGE = 'orange';
|
||||||
|
|
||||||
|
public function dotClass(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::SLATE => 'bg-slate-500',
|
||||||
|
self::AMBER => 'bg-amber-500',
|
||||||
|
self::SKY => 'bg-sky-500',
|
||||||
|
self::VIOLET => 'bg-violet-500',
|
||||||
|
self::TEAL => 'bg-teal-500',
|
||||||
|
self::ROSE => 'bg-rose-500',
|
||||||
|
self::INDIGO => 'bg-indigo-500',
|
||||||
|
self::ORANGE => 'bg-orange-500',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function forId(int $id): self
|
||||||
|
{
|
||||||
|
$cases = self::cases();
|
||||||
|
|
||||||
|
return $cases[$id % count($cases)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\FeedColorEnum;
|
||||||
use Database\Factories\FeedFactory;
|
use Database\Factories\FeedFactory;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -20,6 +21,7 @@
|
||||||
* @property int|null $language_id
|
* @property int|null $language_id
|
||||||
* @property Language|null $language
|
* @property Language|null $language
|
||||||
* @property string $description
|
* @property string $description
|
||||||
|
* @property FeedColorEnum|null $color
|
||||||
* @property array<string, mixed> $settings
|
* @property array<string, mixed> $settings
|
||||||
* @property bool $is_active
|
* @property bool $is_active
|
||||||
* @property Carbon|null $last_fetched_at
|
* @property Carbon|null $last_fetched_at
|
||||||
|
|
@ -46,6 +48,7 @@ class Feed extends Model
|
||||||
'provider',
|
'provider',
|
||||||
'language_id',
|
'language_id',
|
||||||
'description',
|
'description',
|
||||||
|
'color',
|
||||||
'settings',
|
'settings',
|
||||||
'is_active',
|
'is_active',
|
||||||
'last_fetched_at',
|
'last_fetched_at',
|
||||||
|
|
@ -55,8 +58,14 @@ class Feed extends Model
|
||||||
'settings' => 'array',
|
'settings' => 'array',
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
'last_fetched_at' => 'datetime',
|
'last_fetched_at' => 'datetime',
|
||||||
|
'color' => FeedColorEnum::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function displayColor(): FeedColorEnum
|
||||||
|
{
|
||||||
|
return $this->color ?? FeedColorEnum::forId($this->id);
|
||||||
|
}
|
||||||
|
|
||||||
public function getTypeDisplayAttribute(): string
|
public function getTypeDisplayAttribute(): string
|
||||||
{
|
{
|
||||||
return match ($this->type) {
|
return match ($this->type) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?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('feeds', function (Blueprint $table) {
|
||||||
|
$table->string('color')->nullable()->after('description');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('feeds', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('color');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -75,9 +75,9 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-
|
||||||
class="flex items-center flex-wrap gap-x-1.5 text-xs font-medium mb-1.5"
|
class="flex items-center flex-wrap gap-x-1.5 text-xs font-medium mb-1.5"
|
||||||
aria-label="Routed from {{ $routeArticle->feed?->name ?? 'Unknown feed' }} to {{ $routeArticle->platformChannel?->name ?? 'Unknown channel' }}"
|
aria-label="Routed from {{ $routeArticle->feed?->name ?? 'Unknown feed' }} to {{ $routeArticle->platformChannel?->name ?? 'Unknown channel' }}"
|
||||||
>
|
>
|
||||||
<svg class="h-3 w-3 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
@if ($routeArticle->feed)
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
<span class="h-2 w-2 rounded-full shrink-0 {{ $routeArticle->feed->displayColor()->dotClass() }}" aria-hidden="true"></span>
|
||||||
</svg>
|
@endif
|
||||||
<span class="text-gray-900">{{ $routeArticle->feed?->name ?? 'Unknown feed' }}</span>
|
<span class="text-gray-900">{{ $routeArticle->feed?->name ?? 'Unknown feed' }}</span>
|
||||||
<span class="text-gray-400" aria-hidden="true">→</span>
|
<span class="text-gray-400" aria-hidden="true">→</span>
|
||||||
<span class="text-gray-600">{{ $routeArticle->platformChannel?->name ?? 'Unknown channel' }}</span>
|
<span class="text-gray-600">{{ $routeArticle->platformChannel?->name ?? 'Unknown channel' }}</span>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,17 @@ export default {
|
||||||
'./vendor/livewire/livewire/dist/livewire.esm.js',
|
'./vendor/livewire/livewire/dist/livewire.esm.js',
|
||||||
'./storage/framework/views/*.php',
|
'./storage/framework/views/*.php',
|
||||||
],
|
],
|
||||||
|
// Feed dot colours are built in PHP; nothing here is reachable by the scanner.
|
||||||
|
safelist: [
|
||||||
|
'bg-slate-500',
|
||||||
|
'bg-amber-500',
|
||||||
|
'bg-sky-500',
|
||||||
|
'bg-violet-500',
|
||||||
|
'bg-teal-500',
|
||||||
|
'bg-rose-500',
|
||||||
|
'bg-indigo-500',
|
||||||
|
'bg-orange-500',
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Tests\Unit\Models;
|
namespace Tests\Unit\Models;
|
||||||
|
|
||||||
|
use App\Enums\FeedColorEnum;
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\Feed;
|
use App\Models\Feed;
|
||||||
use App\Models\Language;
|
use App\Models\Language;
|
||||||
|
|
@ -17,12 +18,43 @@ class FeedTest extends TestCase
|
||||||
|
|
||||||
public function test_fillable_fields(): void
|
public function test_fillable_fields(): void
|
||||||
{
|
{
|
||||||
$fillableFields = ['name', 'url', 'type', 'provider', 'language_id', 'description', 'settings', 'is_active', 'last_fetched_at'];
|
$fillableFields = ['name', 'url', 'type', 'provider', 'language_id', 'description', 'color', 'settings', 'is_active', 'last_fetched_at'];
|
||||||
$feed = new Feed;
|
$feed = new Feed;
|
||||||
|
|
||||||
$this->assertEquals($fillableFields, $feed->getFillable());
|
$this->assertEquals($fillableFields, $feed->getFillable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_display_color_falls_back_to_a_deterministic_color(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create(['color' => null]);
|
||||||
|
|
||||||
|
$this->assertSame(FeedColorEnum::forId($feed->id), $feed->displayColor());
|
||||||
|
$this->assertSame(FeedColorEnum::forId($feed->id), $feed->fresh()->displayColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_display_color_prefers_the_stored_color(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create(['color' => FeedColorEnum::ROSE]);
|
||||||
|
|
||||||
|
$this->assertSame(FeedColorEnum::ROSE, $feed->fresh()->displayColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_color_fallback_wraps_around_the_palette(): void
|
||||||
|
{
|
||||||
|
$paletteSize = count(FeedColorEnum::cases());
|
||||||
|
|
||||||
|
$this->assertSame(FeedColorEnum::forId(0), FeedColorEnum::forId($paletteSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_every_palette_dot_class_is_safelisted_for_tailwind(): void
|
||||||
|
{
|
||||||
|
$config = (string) file_get_contents(base_path('tailwind.config.js'));
|
||||||
|
|
||||||
|
foreach (FeedColorEnum::cases() as $color) {
|
||||||
|
$this->assertStringContainsString("'{$color->dotClass()}'", $config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function test_casts_settings_to_array(): void
|
public function test_casts_settings_to_array(): void
|
||||||
{
|
{
|
||||||
$settings = ['key1' => 'value1', 'key2' => ['nested' => 'value']];
|
$settings = ['key1' => 'value1', 'key2' => ['nested' => 'value']];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue