mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
Compare commits
4 commits
7014250ac5
...
43f6935348
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43f6935348 | ||
|
|
568bd69fea | ||
|
|
61b9ff56e0 | ||
|
|
73f0fd26c2 |
21 changed files with 1705 additions and 194 deletions
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace App\Jobs;
|
||||
|
||||
use App\Models\DeviceModel;
|
||||
use App\Models\DevicePalette;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
|
@ -20,6 +21,8 @@ final class FetchDeviceModelsJob implements ShouldQueue
|
|||
|
||||
private const API_URL = 'https://usetrmnl.com/api/models';
|
||||
|
||||
private const PALETTES_API_URL = 'http://usetrmnl.com/api/palettes';
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
|
|
@ -34,6 +37,8 @@ final class FetchDeviceModelsJob implements ShouldQueue
|
|||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$this->processPalettes();
|
||||
|
||||
$response = Http::timeout(30)->get(self::API_URL);
|
||||
|
||||
if (! $response->successful()) {
|
||||
|
|
@ -69,6 +74,86 @@ final class FetchDeviceModelsJob implements ShouldQueue
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process palettes from API and update/create records.
|
||||
*/
|
||||
private function processPalettes(): void
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(30)->get(self::PALETTES_API_URL);
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::error('Failed to fetch palettes from API', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $response->json('data', []);
|
||||
|
||||
if (! is_array($data)) {
|
||||
Log::error('Invalid response format from palettes API', [
|
||||
'response' => $response->json(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($data as $paletteData) {
|
||||
try {
|
||||
$this->updateOrCreatePalette($paletteData);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to process palette', [
|
||||
'palette_data' => $paletteData,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('Successfully fetched and updated palettes', [
|
||||
'count' => count($data),
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Exception occurred while fetching palettes', [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or create a palette record.
|
||||
*/
|
||||
private function updateOrCreatePalette(array $paletteData): void
|
||||
{
|
||||
$name = $paletteData['id'] ?? null;
|
||||
|
||||
if (! $name) {
|
||||
Log::warning('Palette data missing id field', [
|
||||
'palette_data' => $paletteData,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$attributes = [
|
||||
'name' => $name,
|
||||
'description' => $paletteData['name'] ?? '',
|
||||
'grays' => $paletteData['grays'] ?? 2,
|
||||
'colors' => $paletteData['colors'] ?? null,
|
||||
'framework_class' => $paletteData['framework_class'] ?? '',
|
||||
'source' => 'api',
|
||||
];
|
||||
|
||||
DevicePalette::updateOrCreate(
|
||||
['name' => $name],
|
||||
$attributes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the device models data and update/create records.
|
||||
*/
|
||||
|
|
@ -117,9 +202,45 @@ final class FetchDeviceModelsJob implements ShouldQueue
|
|||
'source' => 'api',
|
||||
];
|
||||
|
||||
// Set palette_id to the first palette from the model's palettes array
|
||||
$firstPaletteId = $this->getFirstPaletteId($modelData);
|
||||
if ($firstPaletteId) {
|
||||
$attributes['palette_id'] = $firstPaletteId;
|
||||
}
|
||||
|
||||
DeviceModel::updateOrCreate(
|
||||
['name' => $name],
|
||||
$attributes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first palette ID from model data.
|
||||
*/
|
||||
private function getFirstPaletteId(array $modelData): ?int
|
||||
{
|
||||
$paletteName = null;
|
||||
|
||||
// Check for palette_ids array
|
||||
if (isset($modelData['palette_ids']) && is_array($modelData['palette_ids']) && $modelData['palette_ids'] !== []) {
|
||||
$paletteName = $modelData['palette_ids'][0];
|
||||
}
|
||||
|
||||
// Check for palettes array (array of objects with id)
|
||||
if (! $paletteName && isset($modelData['palettes']) && is_array($modelData['palettes']) && $modelData['palettes'] !== []) {
|
||||
$firstPalette = $modelData['palettes'][0];
|
||||
if (is_array($firstPalette) && isset($firstPalette['id'])) {
|
||||
$paletteName = $firstPalette['id'];
|
||||
}
|
||||
}
|
||||
|
||||
if (! $paletteName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Look up palette by name to get the integer ID
|
||||
$palette = DevicePalette::where('name', $paletteName)->first();
|
||||
|
||||
return $palette?->id;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Storage;
|
|||
|
||||
/**
|
||||
* @property-read DeviceModel|null $deviceModel
|
||||
* @property-read DevicePalette|null $palette
|
||||
*/
|
||||
class Device extends Model
|
||||
{
|
||||
|
|
@ -187,6 +188,11 @@ class Device extends Model
|
|||
return $this->belongsTo(DeviceModel::class);
|
||||
}
|
||||
|
||||
public function palette(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DevicePalette::class, 'palette_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color depth string (e.g., "4bit") for the associated device model.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ namespace App\Models;
|
|||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property-read DevicePalette|null $palette
|
||||
*/
|
||||
final class DeviceModel extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
|
@ -35,7 +39,7 @@ final class DeviceModel extends Model
|
|||
return '2bit';
|
||||
}
|
||||
|
||||
// if higher then 4 return 4bit
|
||||
// if higher than 4 return 4bit
|
||||
if ($this->bit_depth > 4) {
|
||||
return '4bit';
|
||||
}
|
||||
|
|
@ -66,4 +70,9 @@ final class DeviceModel extends Model
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function palette(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DevicePalette::class, 'palette_id');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
app/Models/DevicePalette.php
Normal file
23
app/Models/DevicePalette.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property array|null $colors
|
||||
*/
|
||||
final class DevicePalette extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'grays' => 'integer',
|
||||
'colors' => 'array',
|
||||
];
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ class ImageGenerationService
|
|||
{
|
||||
public static function generateImage(string $markup, $deviceId): string
|
||||
{
|
||||
$device = Device::with('deviceModel')->find($deviceId);
|
||||
$device = Device::with(['deviceModel', 'palette', 'deviceModel.palette'])->find($deviceId);
|
||||
$uuid = Uuid::uuid4()->toString();
|
||||
|
||||
try {
|
||||
|
|
@ -61,6 +61,14 @@ class ImageGenerationService
|
|||
$browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']);
|
||||
}
|
||||
|
||||
// Get palette from device or fallback to device model's default palette
|
||||
$palette = $device->palette ?? $device->deviceModel?->palette;
|
||||
$colorPalette = null;
|
||||
|
||||
if ($palette && $palette->colors) {
|
||||
$colorPalette = $palette->colors;
|
||||
}
|
||||
|
||||
$imageStage = new ImageStage();
|
||||
$imageStage->format($fileExtension)
|
||||
->width($imageSettings['width'])
|
||||
|
|
@ -72,6 +80,11 @@ class ImageGenerationService
|
|||
->offsetY($imageSettings['offset_y'])
|
||||
->outputPath($outputPath);
|
||||
|
||||
// Apply color palette if available
|
||||
if ($colorPalette) {
|
||||
$imageStage->colormap($colorPalette);
|
||||
}
|
||||
|
||||
// Apply dithering if requested by markup
|
||||
$shouldDither = self::markupContainsDitherImage($markup);
|
||||
if ($shouldDither) {
|
||||
|
|
@ -338,6 +351,9 @@ class ImageGenerationService
|
|||
$uuid = Uuid::uuid4()->toString();
|
||||
|
||||
try {
|
||||
// Load device with relationships
|
||||
$device->load(['palette', 'deviceModel.palette']);
|
||||
|
||||
// Get image generation settings from DeviceModel if available, otherwise use device settings
|
||||
$imageSettings = self::getImageSettings($device);
|
||||
|
||||
|
|
@ -372,6 +388,14 @@ class ImageGenerationService
|
|||
$browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']);
|
||||
}
|
||||
|
||||
// Get palette from device or fallback to device model's default palette
|
||||
$palette = $device->palette ?? $device->deviceModel?->palette;
|
||||
$colorPalette = null;
|
||||
|
||||
if ($palette && $palette->colors) {
|
||||
$colorPalette = $palette->colors;
|
||||
}
|
||||
|
||||
$imageStage = new ImageStage();
|
||||
$imageStage->format($fileExtension)
|
||||
->width($imageSettings['width'])
|
||||
|
|
@ -383,6 +407,11 @@ class ImageGenerationService
|
|||
->offsetY($imageSettings['offset_y'])
|
||||
->outputPath($outputPath);
|
||||
|
||||
// Apply color palette if available
|
||||
if ($colorPalette) {
|
||||
$imageStage->colormap($colorPalette);
|
||||
}
|
||||
|
||||
(new TrmnlPipeline())->pipe($browserStage)
|
||||
->pipe($imageStage)
|
||||
->process();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"ext-simplexml": "*",
|
||||
"ext-zip": "*",
|
||||
"bnussbau/laravel-trmnl-blade": "2.0.*",
|
||||
"bnussbau/trmnl-pipeline-php": "^0.4.0",
|
||||
"bnussbau/trmnl-pipeline-php": "^0.5.0",
|
||||
"keepsuit/laravel-liquid": "^0.5.2",
|
||||
"laravel/framework": "^12.1",
|
||||
"laravel/sanctum": "^4.0",
|
||||
|
|
|
|||
14
composer.lock
generated
14
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "3d743ce4dc2742c59ed6f9cc8ed36e04",
|
||||
"content-hash": "38e8a7dd90ccc1b777a4c8a5a28f9f14",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
|
|
@ -243,16 +243,16 @@
|
|||
},
|
||||
{
|
||||
"name": "bnussbau/trmnl-pipeline-php",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bnussbau/trmnl-pipeline-php.git",
|
||||
"reference": "b58b937f36e55aa7cbd4859cbe7a902bf1050bf8"
|
||||
"reference": "eb55b89e1f3991764912505872bbce809874d1aa"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/b58b937f36e55aa7cbd4859cbe7a902bf1050bf8",
|
||||
"reference": "b58b937f36e55aa7cbd4859cbe7a902bf1050bf8",
|
||||
"url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/eb55b89e1f3991764912505872bbce809874d1aa",
|
||||
"reference": "eb55b89e1f3991764912505872bbce809874d1aa",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -294,7 +294,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/bnussbau/trmnl-pipeline-php/issues",
|
||||
"source": "https://github.com/bnussbau/trmnl-pipeline-php/tree/0.4.0"
|
||||
"source": "https://github.com/bnussbau/trmnl-pipeline-php/tree/0.5.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -310,7 +310,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-10-30T11:52:17+00:00"
|
||||
"time": "2025-11-25T17:00:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
|
|
|||
38
database/factories/DevicePaletteFactory.php
Normal file
38
database/factories/DevicePaletteFactory.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\DevicePalette;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\DevicePalette>
|
||||
*/
|
||||
class DevicePaletteFactory extends Factory
|
||||
{
|
||||
protected $model = DevicePalette::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => 'test-' . $this->faker->unique()->slug(),
|
||||
'name' => $this->faker->words(3, true),
|
||||
'grays' => $this->faker->randomElement([2, 4, 16, 256]),
|
||||
'colors' => $this->faker->optional()->passthrough([
|
||||
'#FF0000',
|
||||
'#00FF00',
|
||||
'#0000FF',
|
||||
'#FFFF00',
|
||||
'#000000',
|
||||
'#FFFFFF',
|
||||
]),
|
||||
'framework_class' => null,
|
||||
'source' => 'api',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ return new class extends Migration
|
|||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropUnique(['oidc_sub']);
|
||||
$table->dropColumn('oidc_sub');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('device_palettes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('description')->nullable();
|
||||
$table->integer('grays');
|
||||
$table->json('colors')->nullable();
|
||||
$table->string('framework_class')->default('');
|
||||
$table->string('source')->default('api');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('device_palettes');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('device_models', function (Blueprint $table) {
|
||||
$table->foreignId('palette_id')->nullable()->after('source')->constrained('device_palettes')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('device_models', function (Blueprint $table) {
|
||||
$table->dropForeign(['palette_id']);
|
||||
$table->dropColumn('palette_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
$table->foreignId('palette_id')->nullable()->constrained('device_palettes')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
$table->dropForeign(['palette_id']);
|
||||
$table->dropColumn('palette_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
use App\Models\DeviceModel;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Seed palettes from hardcoded data
|
||||
// name = identifier, description = human-readable name
|
||||
$palettes = [
|
||||
[
|
||||
'name' => 'bw',
|
||||
'description' => 'Black & White',
|
||||
'grays' => 2,
|
||||
'colors' => null,
|
||||
'framework_class' => 'screen--1bit',
|
||||
'source' => 'api',
|
||||
],
|
||||
[
|
||||
'name' => 'gray-4',
|
||||
'description' => '4 Grays',
|
||||
'grays' => 4,
|
||||
'colors' => null,
|
||||
'framework_class' => 'screen--2bit',
|
||||
'source' => 'api',
|
||||
],
|
||||
[
|
||||
'name' => 'gray-16',
|
||||
'description' => '16 Grays',
|
||||
'grays' => 16,
|
||||
'colors' => null,
|
||||
'framework_class' => 'screen--4bit',
|
||||
'source' => 'api',
|
||||
],
|
||||
[
|
||||
'name' => 'gray-256',
|
||||
'description' => '256 Grays',
|
||||
'grays' => 256,
|
||||
'colors' => null,
|
||||
'framework_class' => 'screen--4bit',
|
||||
'source' => 'api',
|
||||
],
|
||||
[
|
||||
'name' => 'color-6a',
|
||||
'description' => '6 Colors',
|
||||
'grays' => 2,
|
||||
'colors' => json_encode(['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#000000', '#FFFFFF']),
|
||||
'framework_class' => '',
|
||||
'source' => 'api',
|
||||
],
|
||||
[
|
||||
'name' => 'color-7a',
|
||||
'description' => '7 Colors',
|
||||
'grays' => 2,
|
||||
'colors' => json_encode(['#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FFA500']),
|
||||
'framework_class' => '',
|
||||
'source' => 'api',
|
||||
],
|
||||
];
|
||||
|
||||
$now = now();
|
||||
$paletteIdMap = [];
|
||||
|
||||
foreach ($palettes as $paletteData) {
|
||||
$paletteName = $paletteData['name'];
|
||||
$paletteData['created_at'] = $now;
|
||||
$paletteData['updated_at'] = $now;
|
||||
|
||||
DB::table('device_palettes')->updateOrInsert(
|
||||
['name' => $paletteName],
|
||||
$paletteData
|
||||
);
|
||||
|
||||
// Get the ID of the palette (either newly created or existing)
|
||||
$paletteRecord = DB::table('device_palettes')->where('name', $paletteName)->first();
|
||||
$paletteIdMap[$paletteName] = $paletteRecord->id;
|
||||
}
|
||||
|
||||
// Set default palette_id on DeviceModel based on first palette_ids entry
|
||||
$models = [
|
||||
['name' => 'og_png', 'palette_name' => 'bw'],
|
||||
['name' => 'og_plus', 'palette_name' => 'gray-4'],
|
||||
['name' => 'amazon_kindle_2024', 'palette_name' => 'gray-256'],
|
||||
['name' => 'amazon_kindle_paperwhite_6th_gen', 'palette_name' => 'gray-256'],
|
||||
['name' => 'amazon_kindle_paperwhite_7th_gen', 'palette_name' => 'gray-256'],
|
||||
['name' => 'inkplate_10', 'palette_name' => 'gray-4'],
|
||||
['name' => 'amazon_kindle_7', 'palette_name' => 'gray-256'],
|
||||
['name' => 'inky_impression_7_3', 'palette_name' => 'color-7a'],
|
||||
['name' => 'kobo_libra_2', 'palette_name' => 'gray-16'],
|
||||
['name' => 'amazon_kindle_oasis_2', 'palette_name' => 'gray-256'],
|
||||
['name' => 'kobo_aura_one', 'palette_name' => 'gray-16'],
|
||||
['name' => 'kobo_aura_hd', 'palette_name' => 'gray-16'],
|
||||
['name' => 'inky_impression_13_3', 'palette_name' => 'color-6a'],
|
||||
['name' => 'm5_paper_s3', 'palette_name' => 'gray-16'],
|
||||
['name' => 'amazon_kindle_scribe', 'palette_name' => 'gray-256'],
|
||||
['name' => 'seeed_e1001', 'palette_name' => 'gray-4'],
|
||||
['name' => 'seeed_e1002', 'palette_name' => 'gray-4'],
|
||||
['name' => 'waveshare_4_26', 'palette_name' => 'gray-4'],
|
||||
['name' => 'waveshare_7_5_bw', 'palette_name' => 'bw'],
|
||||
];
|
||||
|
||||
foreach ($models as $modelData) {
|
||||
$deviceModel = DeviceModel::where('name', $modelData['name'])->first();
|
||||
if ($deviceModel && ! $deviceModel->palette_id && isset($paletteIdMap[$modelData['palette_name']])) {
|
||||
$deviceModel->update(['palette_id' => $paletteIdMap[$modelData['palette_name']]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Remove palette_id from device models but keep palettes
|
||||
DeviceModel::query()->update(['palette_id' => null]);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,26 +1,43 @@
|
|||
<?php
|
||||
|
||||
use App\Models\DeviceModel;
|
||||
use App\Models\DevicePalette;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
public $deviceModels;
|
||||
|
||||
public $devicePalettes;
|
||||
|
||||
public $name;
|
||||
|
||||
public $label;
|
||||
|
||||
public $description;
|
||||
|
||||
public $width;
|
||||
|
||||
public $height;
|
||||
|
||||
public $colors;
|
||||
|
||||
public $bit_depth;
|
||||
|
||||
public $scale_factor = 1.0;
|
||||
|
||||
public $rotation = 0;
|
||||
|
||||
public $mime_type = 'image/png';
|
||||
|
||||
public $offset_x = 0;
|
||||
|
||||
public $offset_y = 0;
|
||||
|
||||
public $published_at;
|
||||
|
||||
public $palette_id;
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|string|max:255|unique:device_models,name',
|
||||
'label' => 'required|string|max:255',
|
||||
|
|
@ -40,41 +57,28 @@ new class extends Component {
|
|||
public function mount()
|
||||
{
|
||||
$this->deviceModels = DeviceModel::all();
|
||||
$this->devicePalettes = DevicePalette::all();
|
||||
|
||||
return view('livewire.device-models.index');
|
||||
}
|
||||
|
||||
public function createDeviceModel(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
DeviceModel::create([
|
||||
'name' => $this->name,
|
||||
'label' => $this->label,
|
||||
'description' => $this->description,
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
'colors' => $this->colors,
|
||||
'bit_depth' => $this->bit_depth,
|
||||
'scale_factor' => $this->scale_factor,
|
||||
'rotation' => $this->rotation,
|
||||
'mime_type' => $this->mime_type,
|
||||
'offset_x' => $this->offset_x,
|
||||
'offset_y' => $this->offset_y,
|
||||
'published_at' => $this->published_at,
|
||||
]);
|
||||
|
||||
$this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at']);
|
||||
\Flux::modal('create-device-model')->close();
|
||||
|
||||
$this->deviceModels = DeviceModel::all();
|
||||
session()->flash('message', 'Device model created successfully.');
|
||||
}
|
||||
|
||||
public $editingDeviceModelId;
|
||||
|
||||
public function editDeviceModel(DeviceModel $deviceModel): void
|
||||
public $viewingDeviceModelId;
|
||||
|
||||
public function openDeviceModelModal(?string $deviceModelId = null, bool $viewOnly = false): void
|
||||
{
|
||||
if ($deviceModelId) {
|
||||
$deviceModel = DeviceModel::findOrFail($deviceModelId);
|
||||
|
||||
if ($viewOnly) {
|
||||
$this->viewingDeviceModelId = $deviceModel->id;
|
||||
$this->editingDeviceModelId = null;
|
||||
} else {
|
||||
$this->editingDeviceModelId = $deviceModel->id;
|
||||
$this->viewingDeviceModelId = null;
|
||||
}
|
||||
|
||||
$this->name = $deviceModel->name;
|
||||
$this->label = $deviceModel->label;
|
||||
$this->description = $deviceModel->description;
|
||||
|
|
@ -88,14 +92,23 @@ new class extends Component {
|
|||
$this->offset_x = $deviceModel->offset_x;
|
||||
$this->offset_y = $deviceModel->offset_y;
|
||||
$this->published_at = $deviceModel->published_at?->format('Y-m-d\TH:i');
|
||||
$this->palette_id = $deviceModel->palette_id;
|
||||
} else {
|
||||
$this->editingDeviceModelId = null;
|
||||
$this->viewingDeviceModelId = null;
|
||||
$this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'palette_id']);
|
||||
$this->mime_type = 'image/png';
|
||||
$this->scale_factor = 1.0;
|
||||
$this->rotation = 0;
|
||||
$this->offset_x = 0;
|
||||
$this->offset_y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public function updateDeviceModel(): void
|
||||
public function saveDeviceModel(): void
|
||||
{
|
||||
$deviceModel = DeviceModel::findOrFail($this->editingDeviceModelId);
|
||||
|
||||
$this->validate([
|
||||
'name' => 'required|string|max:255|unique:device_models,name,' . $deviceModel->id,
|
||||
$rules = [
|
||||
'name' => 'required|string|max:255',
|
||||
'label' => 'required|string|max:255',
|
||||
'description' => 'required|string',
|
||||
'width' => 'required|integer|min:1',
|
||||
|
|
@ -108,8 +121,19 @@ new class extends Component {
|
|||
'offset_x' => 'required|integer',
|
||||
'offset_y' => 'required|integer',
|
||||
'published_at' => 'nullable|date',
|
||||
]);
|
||||
'palette_id' => 'nullable|exists:device_palettes,id',
|
||||
];
|
||||
|
||||
if ($this->editingDeviceModelId) {
|
||||
$rules['name'] = 'required|string|max:255|unique:device_models,name,'.$this->editingDeviceModelId;
|
||||
} else {
|
||||
$rules['name'] = 'required|string|max:255|unique:device_models,name';
|
||||
}
|
||||
|
||||
$this->validate($rules);
|
||||
|
||||
if ($this->editingDeviceModelId) {
|
||||
$deviceModel = DeviceModel::findOrFail($this->editingDeviceModelId);
|
||||
$deviceModel->update([
|
||||
'name' => $this->name,
|
||||
'label' => $this->label,
|
||||
|
|
@ -124,22 +148,69 @@ new class extends Component {
|
|||
'offset_x' => $this->offset_x,
|
||||
'offset_y' => $this->offset_y,
|
||||
'published_at' => $this->published_at,
|
||||
'palette_id' => $this->palette_id ?: null,
|
||||
]);
|
||||
|
||||
$this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'editingDeviceModelId']);
|
||||
\Flux::modal('edit-device-model-' . $deviceModel->id)->close();
|
||||
|
||||
$this->deviceModels = DeviceModel::all();
|
||||
session()->flash('message', 'Device model updated successfully.');
|
||||
$message = 'Device model updated successfully.';
|
||||
} else {
|
||||
DeviceModel::create([
|
||||
'name' => $this->name,
|
||||
'label' => $this->label,
|
||||
'description' => $this->description,
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
'colors' => $this->colors,
|
||||
'bit_depth' => $this->bit_depth,
|
||||
'scale_factor' => $this->scale_factor,
|
||||
'rotation' => $this->rotation,
|
||||
'mime_type' => $this->mime_type,
|
||||
'offset_x' => $this->offset_x,
|
||||
'offset_y' => $this->offset_y,
|
||||
'published_at' => $this->published_at,
|
||||
'palette_id' => $this->palette_id ?: null,
|
||||
'source' => 'manual',
|
||||
]);
|
||||
$message = 'Device model created successfully.';
|
||||
}
|
||||
|
||||
public function deleteDeviceModel(DeviceModel $deviceModel): void
|
||||
$this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'palette_id', 'editingDeviceModelId', 'viewingDeviceModelId']);
|
||||
Flux::modal('device-model-modal')->close();
|
||||
|
||||
$this->deviceModels = DeviceModel::all();
|
||||
session()->flash('message', $message);
|
||||
}
|
||||
|
||||
public function deleteDeviceModel(string $deviceModelId): void
|
||||
{
|
||||
$deviceModel = DeviceModel::findOrFail($deviceModelId);
|
||||
$deviceModel->delete();
|
||||
|
||||
$this->deviceModels = DeviceModel::all();
|
||||
session()->flash('message', 'Device model deleted successfully.');
|
||||
}
|
||||
|
||||
public function duplicateDeviceModel(string $deviceModelId): void
|
||||
{
|
||||
$deviceModel = DeviceModel::findOrFail($deviceModelId);
|
||||
|
||||
$this->editingDeviceModelId = null;
|
||||
$this->viewingDeviceModelId = null;
|
||||
$this->name = $deviceModel->name.' (Copy)';
|
||||
$this->label = $deviceModel->label;
|
||||
$this->description = $deviceModel->description;
|
||||
$this->width = $deviceModel->width;
|
||||
$this->height = $deviceModel->height;
|
||||
$this->colors = $deviceModel->colors;
|
||||
$this->bit_depth = $deviceModel->bit_depth;
|
||||
$this->scale_factor = $deviceModel->scale_factor;
|
||||
$this->rotation = $deviceModel->rotation;
|
||||
$this->mime_type = $deviceModel->mime_type;
|
||||
$this->offset_x = $deviceModel->offset_x;
|
||||
$this->offset_y = $deviceModel->offset_y;
|
||||
$this->published_at = $deviceModel->published_at?->format('Y-m-d\TH:i');
|
||||
$this->palette_id = $deviceModel->palette_id;
|
||||
|
||||
$this->js('Flux.modal("device-model-modal").show()');
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
@ -148,10 +219,19 @@ new class extends Component {
|
|||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<h2 class="text-2xl font-semibold dark:text-gray-100">Device Models</h2>
|
||||
{{-- <flux:modal.trigger name="create-device-model">--}}
|
||||
{{-- <flux:button icon="plus" variant="primary">Add Device Model</flux:button>--}}
|
||||
{{-- </flux:modal.trigger>--}}
|
||||
<flux:dropdown>
|
||||
<flux:button icon="chevron-down" variant="ghost"></flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item href="{{ route('devices') }}">Devices</flux:menu.item>
|
||||
<flux:menu.item href="{{ route('device-palettes.index') }}">Device Palettes</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</div>
|
||||
<flux:modal.trigger name="device-model-modal">
|
||||
<flux:button wire:click="openDeviceModelModal()" icon="plus" variant="primary">Add Device Model</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
@if (session()->has('message'))
|
||||
<div class="mb-4">
|
||||
|
|
@ -164,156 +244,103 @@ new class extends Component {
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<flux:modal name="create-device-model" class="md:w-96">
|
||||
<flux:modal name="device-model-modal" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">Add Device Model</flux:heading>
|
||||
<flux:heading size="lg">
|
||||
@if ($viewingDeviceModelId)
|
||||
View Device Model
|
||||
@elseif ($editingDeviceModelId)
|
||||
Edit Device Model
|
||||
@else
|
||||
Add Device Model
|
||||
@endif
|
||||
</flux:heading>
|
||||
</div>
|
||||
|
||||
<form wire:submit="createDeviceModel">
|
||||
<form wire:submit="saveDeviceModel">
|
||||
<div class="mb-4">
|
||||
<flux:input label="Name" wire:model="name" id="name" class="block mt-1 w-full" type="text"
|
||||
name="name" autofocus/>
|
||||
name="name" autofocus :disabled="(bool) $viewingDeviceModelId"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input label="Label" wire:model="label" id="label" class="block mt-1 w-full"
|
||||
type="text"
|
||||
name="label"/>
|
||||
name="label" :disabled="(bool) $viewingDeviceModelId"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input label="Description" wire:model="description" id="description"
|
||||
class="block mt-1 w-full" name="description"/>
|
||||
class="block mt-1 w-full" name="description" :disabled="(bool) $viewingDeviceModelId"/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<flux:input label="Width" wire:model="width" id="width" class="block mt-1 w-full"
|
||||
type="number"
|
||||
name="width"/>
|
||||
name="width" :disabled="(bool) $viewingDeviceModelId"/>
|
||||
<flux:input label="Height" wire:model="height" id="height" class="block mt-1 w-full"
|
||||
type="number"
|
||||
name="height"/>
|
||||
name="height" :disabled="(bool) $viewingDeviceModelId"/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<flux:input label="Colors" wire:model="colors" id="colors" class="block mt-1 w-full"
|
||||
type="number"
|
||||
name="colors"/>
|
||||
name="colors" :disabled="(bool) $viewingDeviceModelId"/>
|
||||
<flux:input label="Bit Depth" wire:model="bit_depth" id="bit_depth"
|
||||
class="block mt-1 w-full" type="number"
|
||||
name="bit_depth"/>
|
||||
name="bit_depth" :disabled="(bool) $viewingDeviceModelId"/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<flux:input label="Scale Factor" wire:model="scale_factor" id="scale_factor"
|
||||
class="block mt-1 w-full" type="number"
|
||||
name="scale_factor" step="0.1"/>
|
||||
name="scale_factor" step="0.1" :disabled="(bool) $viewingDeviceModelId"/>
|
||||
<flux:input label="Rotation" wire:model="rotation" id="rotation" class="block mt-1 w-full"
|
||||
type="number"
|
||||
name="rotation"/>
|
||||
name="rotation" :disabled="(bool) $viewingDeviceModelId"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input label="MIME Type" wire:model="mime_type" id="mime_type"
|
||||
class="block mt-1 w-full" type="text"
|
||||
name="mime_type"/>
|
||||
<flux:select label="MIME Type" wire:model="mime_type" id="mime_type" name="mime_type" :disabled="(bool) $viewingDeviceModelId">
|
||||
<flux:select.option>image/png</flux:select.option>
|
||||
<flux:select.option>image/bmp</flux:select.option>
|
||||
</flux:select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<flux:input label="Offset X" wire:model="offset_x" id="offset_x" class="block mt-1 w-full"
|
||||
type="number"
|
||||
name="offset_x"/>
|
||||
name="offset_x" :disabled="(bool) $viewingDeviceModelId"/>
|
||||
<flux:input label="Offset Y" wire:model="offset_y" id="offset_y" class="block mt-1 w-full"
|
||||
type="number"
|
||||
name="offset_y"/>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
<flux:button type="submit" variant="primary">Create Device Model</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
@foreach ($deviceModels as $deviceModel)
|
||||
<flux:modal name="edit-device-model-{{ $deviceModel->id }}" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">Edit Device Model</flux:heading>
|
||||
</div>
|
||||
|
||||
<form wire:submit="updateDeviceModel">
|
||||
<div class="mb-4">
|
||||
<flux:input label="Name" wire:model="name" id="edit_name" class="block mt-1 w-full"
|
||||
type="text"
|
||||
name="edit_name"/>
|
||||
name="offset_y" :disabled="(bool) $viewingDeviceModelId"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input label="Label" wire:model="label" id="edit_label" class="block mt-1 w-full"
|
||||
type="text"
|
||||
name="edit_label"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input label="Description" wire:model="description" id="edit_description"
|
||||
class="block mt-1 w-full" name="edit_description"/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<flux:input label="Width" wire:model="width" id="edit_width" class="block mt-1 w-full"
|
||||
type="number"
|
||||
name="edit_width"/>
|
||||
<flux:input label="Height" wire:model="height" id="edit_height"
|
||||
class="block mt-1 w-full" type="number"
|
||||
name="edit_height"/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<flux:input label="Colors" wire:model="colors" id="edit_colors"
|
||||
class="block mt-1 w-full" type="number"
|
||||
name="edit_colors"/>
|
||||
<flux:input label="Bit Depth" wire:model="bit_depth" id="edit_bit_depth"
|
||||
class="block mt-1 w-full" type="number"
|
||||
name="edit_bit_depth"/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<flux:input label="Scale Factor" wire:model="scale_factor" id="edit_scale_factor"
|
||||
class="block mt-1 w-full" type="number"
|
||||
name="edit_scale_factor" step="0.1"/>
|
||||
<flux:input label="Rotation" wire:model="rotation" id="edit_rotation"
|
||||
class="block mt-1 w-full" type="number"
|
||||
name="edit_rotation"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:select label="MIME Type" wire:model="mime_type" id="edit_mime_type" name="edit_mime_type">
|
||||
<flux:select.option>image/png</flux:select.option>
|
||||
<flux:select.option>image/bmp</flux:select.option>
|
||||
</flux:select>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<flux:input label="Offset X" wire:model="offset_x" id="edit_offset_x"
|
||||
class="block mt-1 w-full" type="number"
|
||||
name="edit_offset_x"/>
|
||||
<flux:input label="Offset Y" wire:model="offset_y" id="edit_offset_y"
|
||||
class="block mt-1 w-full" type="number"
|
||||
name="edit_offset_y"/>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
<flux:button type="submit" variant="primary">Update Device Model</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
<flux:select label="Color Palette" wire:model="palette_id" id="palette_id" name="palette_id" :disabled="(bool) $viewingDeviceModelId">
|
||||
<flux:select.option value="">None</flux:select.option>
|
||||
@foreach ($devicePalettes as $palette)
|
||||
<flux:select.option value="{{ $palette->id }}">{{ $palette->description ?? $palette->name }} ({{ $palette->name }})</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</div>
|
||||
|
||||
@if (!$viewingDeviceModelId)
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
<flux:button type="submit" variant="primary">{{ $editingDeviceModelId ? 'Update' : 'Create' }} Device Model</flux:button>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
<flux:button type="button" wire:click="duplicateDeviceModel({{ $viewingDeviceModelId }})" variant="primary">Duplicate</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
<table
|
||||
class="min-w-full table-fixed text-zinc-800 divide-y divide-zinc-800/10 dark:divide-white/20 text-zinc-800"
|
||||
|
|
@ -369,14 +396,25 @@ new class extends Component {
|
|||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:button.group>
|
||||
<flux:modal.trigger name="edit-device-model-{{ $deviceModel->id }}">
|
||||
<flux:button wire:click="editDeviceModel({{ $deviceModel->id }})" icon="pencil"
|
||||
@if ($deviceModel->source === 'api')
|
||||
<flux:modal.trigger name="device-model-modal">
|
||||
<flux:button wire:click="openDeviceModelModal('{{ $deviceModel->id }}', true)" icon="eye"
|
||||
iconVariant="outline">
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
<flux:button wire:click="deleteDeviceModel({{ $deviceModel->id }})" icon="trash"
|
||||
<flux:button wire:click="duplicateDeviceModel('{{ $deviceModel->id }}')" icon="document-duplicate"
|
||||
iconVariant="outline">
|
||||
</flux:button>
|
||||
@else
|
||||
<flux:modal.trigger name="device-model-modal">
|
||||
<flux:button wire:click="openDeviceModelModal('{{ $deviceModel->id }}')" icon="pencil"
|
||||
iconVariant="outline">
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
<flux:button wire:click="deleteDeviceModel('{{ $deviceModel->id }}')" icon="trash"
|
||||
iconVariant="outline">
|
||||
</flux:button>
|
||||
@endif
|
||||
</flux:button.group>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
|||
384
resources/views/livewire/device-palettes/index.blade.php
Normal file
384
resources/views/livewire/device-palettes/index.blade.php
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
<?php
|
||||
|
||||
use App\Models\DevicePalette;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
public $devicePalettes;
|
||||
|
||||
public $name;
|
||||
|
||||
public $description;
|
||||
|
||||
public $grays = 2;
|
||||
|
||||
public $colors = [];
|
||||
|
||||
public $framework_class = '';
|
||||
|
||||
public $colorInput = '';
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|string|max:255|unique:device_palettes,name',
|
||||
'description' => 'nullable|string|max:255',
|
||||
'grays' => 'required|integer|min:1|max:256',
|
||||
'colors' => 'nullable|array',
|
||||
'colors.*' => 'string|regex:/^#[0-9A-Fa-f]{6}$/',
|
||||
'framework_class' => 'nullable|string|max:255',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->devicePalettes = DevicePalette::all();
|
||||
|
||||
return view('livewire.device-palettes.index');
|
||||
}
|
||||
|
||||
public function addColor(): void
|
||||
{
|
||||
$this->validate(['colorInput' => 'required|string|regex:/^#[0-9A-Fa-f]{6}$/'], [
|
||||
'colorInput.regex' => 'Color must be a valid hex color (e.g., #FF0000)',
|
||||
]);
|
||||
|
||||
if (! in_array($this->colorInput, $this->colors)) {
|
||||
$this->colors[] = $this->colorInput;
|
||||
}
|
||||
|
||||
$this->colorInput = '';
|
||||
}
|
||||
|
||||
public function removeColor(int $index): void
|
||||
{
|
||||
unset($this->colors[$index]);
|
||||
$this->colors = array_values($this->colors);
|
||||
}
|
||||
|
||||
public $editingDevicePaletteId;
|
||||
|
||||
public $viewingDevicePaletteId;
|
||||
|
||||
public function openDevicePaletteModal(?string $devicePaletteId = null, bool $viewOnly = false): void
|
||||
{
|
||||
if ($devicePaletteId) {
|
||||
$devicePalette = DevicePalette::findOrFail($devicePaletteId);
|
||||
|
||||
if ($viewOnly) {
|
||||
$this->viewingDevicePaletteId = $devicePalette->id;
|
||||
$this->editingDevicePaletteId = null;
|
||||
} else {
|
||||
$this->editingDevicePaletteId = $devicePalette->id;
|
||||
$this->viewingDevicePaletteId = null;
|
||||
}
|
||||
|
||||
$this->name = $devicePalette->name;
|
||||
$this->description = $devicePalette->description;
|
||||
$this->grays = $devicePalette->grays;
|
||||
|
||||
// Ensure colors is always an array and properly decoded
|
||||
// The model cast should handle JSON decoding, but we'll be explicit
|
||||
$colors = $devicePalette->getAttribute('colors');
|
||||
|
||||
if ($colors === null) {
|
||||
$this->colors = [];
|
||||
} elseif (is_string($colors)) {
|
||||
$decoded = json_decode($colors, true);
|
||||
$this->colors = is_array($decoded) ? array_values($decoded) : [];
|
||||
} elseif (is_array($colors)) {
|
||||
$this->colors = array_values($colors); // Re-index array
|
||||
} else {
|
||||
$this->colors = [];
|
||||
}
|
||||
|
||||
$this->framework_class = $devicePalette->framework_class;
|
||||
} else {
|
||||
$this->editingDevicePaletteId = null;
|
||||
$this->viewingDevicePaletteId = null;
|
||||
$this->reset(['name', 'description', 'grays', 'colors', 'framework_class']);
|
||||
}
|
||||
|
||||
$this->colorInput = '';
|
||||
}
|
||||
|
||||
public function saveDevicePalette(): void
|
||||
{
|
||||
$rules = [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string|max:255',
|
||||
'grays' => 'required|integer|min:1|max:256',
|
||||
'colors' => 'nullable|array',
|
||||
'colors.*' => 'string|regex:/^#[0-9A-Fa-f]{6}$/',
|
||||
'framework_class' => 'nullable|string|max:255',
|
||||
];
|
||||
|
||||
if ($this->editingDevicePaletteId) {
|
||||
$rules['name'] = 'required|string|max:255|unique:device_palettes,name,'.$this->editingDevicePaletteId;
|
||||
} else {
|
||||
$rules['name'] = 'required|string|max:255|unique:device_palettes,name';
|
||||
}
|
||||
|
||||
$this->validate($rules);
|
||||
|
||||
if ($this->editingDevicePaletteId) {
|
||||
$devicePalette = DevicePalette::findOrFail($this->editingDevicePaletteId);
|
||||
$devicePalette->update([
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'grays' => $this->grays,
|
||||
'colors' => ! empty($this->colors) ? $this->colors : null,
|
||||
'framework_class' => $this->framework_class,
|
||||
]);
|
||||
$message = 'Device palette updated successfully.';
|
||||
} else {
|
||||
DevicePalette::create([
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'grays' => $this->grays,
|
||||
'colors' => ! empty($this->colors) ? $this->colors : null,
|
||||
'framework_class' => $this->framework_class,
|
||||
'source' => 'manual',
|
||||
]);
|
||||
$message = 'Device palette created successfully.';
|
||||
}
|
||||
|
||||
$this->reset(['name', 'description', 'grays', 'colors', 'framework_class', 'colorInput', 'editingDevicePaletteId', 'viewingDevicePaletteId']);
|
||||
Flux::modal('device-palette-modal')->close();
|
||||
|
||||
$this->devicePalettes = DevicePalette::all();
|
||||
session()->flash('message', $message);
|
||||
}
|
||||
|
||||
public function deleteDevicePalette(string $devicePaletteId): void
|
||||
{
|
||||
$devicePalette = DevicePalette::findOrFail($devicePaletteId);
|
||||
$devicePalette->delete();
|
||||
|
||||
$this->devicePalettes = DevicePalette::all();
|
||||
session()->flash('message', 'Device palette deleted successfully.');
|
||||
}
|
||||
|
||||
public function duplicateDevicePalette(string $devicePaletteId): void
|
||||
{
|
||||
$devicePalette = DevicePalette::findOrFail($devicePaletteId);
|
||||
|
||||
$this->editingDevicePaletteId = null;
|
||||
$this->viewingDevicePaletteId = null;
|
||||
$this->name = $devicePalette->name.' (Copy)';
|
||||
$this->description = $devicePalette->description;
|
||||
$this->grays = $devicePalette->grays;
|
||||
|
||||
$colors = $devicePalette->getAttribute('colors');
|
||||
if ($colors === null) {
|
||||
$this->colors = [];
|
||||
} elseif (is_string($colors)) {
|
||||
$decoded = json_decode($colors, true);
|
||||
$this->colors = is_array($decoded) ? array_values($decoded) : [];
|
||||
} elseif (is_array($colors)) {
|
||||
$this->colors = array_values($colors);
|
||||
} else {
|
||||
$this->colors = [];
|
||||
}
|
||||
|
||||
$this->framework_class = $devicePalette->framework_class;
|
||||
$this->colorInput = '';
|
||||
|
||||
$this->js('Flux.modal("device-palette-modal").show()');
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div>
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<h2 class="text-2xl font-semibold dark:text-gray-100">Device Palettes</h2>
|
||||
<flux:dropdown>
|
||||
<flux:button icon="chevron-down" variant="ghost"></flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item href="{{ route('devices') }}">Devices</flux:menu.item>
|
||||
<flux:menu.item href="{{ route('device-models.index') }}">Device Models</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</div>
|
||||
<flux:modal.trigger name="device-palette-modal">
|
||||
<flux:button wire:click="openDevicePaletteModal()" icon="plus" variant="primary">Add Device Palette</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
@if (session()->has('message'))
|
||||
<div class="mb-4">
|
||||
<flux:callout variant="success" icon="check-circle" heading=" {{ session('message') }}">
|
||||
<x-slot name="controls">
|
||||
<flux:button icon="x-mark" variant="ghost"
|
||||
x-on:click="$el.closest('[data-flux-callout]').remove()"/>
|
||||
</x-slot>
|
||||
</flux:callout>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:modal name="device-palette-modal" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">
|
||||
@if ($viewingDevicePaletteId)
|
||||
View Device Palette
|
||||
@elseif ($editingDevicePaletteId)
|
||||
Edit Device Palette
|
||||
@else
|
||||
Add Device Palette
|
||||
@endif
|
||||
</flux:heading>
|
||||
</div>
|
||||
|
||||
<form wire:submit="saveDevicePalette">
|
||||
<div class="mb-4">
|
||||
<flux:input label="Name (Identifier)" wire:model="name" id="name" class="block mt-1 w-full" type="text"
|
||||
name="name" autofocus :disabled="$viewingDevicePaletteId"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input label="Description" wire:model="description" id="description" class="block mt-1 w-full" type="text"
|
||||
name="description" :disabled="$viewingDevicePaletteId"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input label="Grays" wire:model="grays" id="grays" class="block mt-1 w-full"
|
||||
type="number"
|
||||
name="grays" min="1" max="256" :disabled="$viewingDevicePaletteId"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:input label="Framework Class" wire:model="framework_class" id="framework_class"
|
||||
class="block mt-1 w-full" type="text"
|
||||
name="framework_class" :disabled="$viewingDevicePaletteId"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<flux:label>Colors</flux:label>
|
||||
@if (!$viewingDevicePaletteId)
|
||||
<div class="flex gap-2 mb-2">
|
||||
<flux:input wire:model="colorInput" placeholder="#FF0000" class="flex-1"/>
|
||||
<flux:button type="button" wire:click="addColor" variant="ghost">Add</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if (!empty($colors) && is_array($colors) && count($colors) > 0)
|
||||
@foreach ($colors as $index => $color)
|
||||
@if (!empty($color))
|
||||
<div wire:key="color-{{ $editingDevicePaletteId ?? $viewingDevicePaletteId ?? 'new' }}-{{ $index }}-{{ $color }}" class="flex items-center gap-2 px-3 py-1 bg-zinc-100 dark:bg-zinc-800 rounded">
|
||||
<div class="w-4 h-4 rounded border border-zinc-300 dark:border-zinc-600" style="background-color: {{ $color }}"></div>
|
||||
<span class="text-sm">{{ $color }}</span>
|
||||
@if (!$viewingDevicePaletteId)
|
||||
<flux:button type="button" wire:click="removeColor({{ $index }})" icon="x-mark" variant="ghost" size="sm"></flux:button>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
@if (!$viewingDevicePaletteId)
|
||||
<p class="mt-1 text-xs text-zinc-500">Leave empty for grayscale-only palette</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (!$viewingDevicePaletteId)
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
<flux:button type="submit" variant="primary">{{ $editingDevicePaletteId ? 'Update' : 'Create' }} Device Palette</flux:button>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex">
|
||||
<flux:spacer/>
|
||||
<flux:button type="button" wire:click="duplicateDevicePalette('{{ $viewingDevicePaletteId }}')" variant="primary">Duplicate</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
<table
|
||||
class="min-w-full table-fixed text-zinc-800 divide-y divide-zinc-800/10 dark:divide-white/20 text-zinc-800"
|
||||
data-flux-table>
|
||||
<thead data-flux-columns>
|
||||
<tr>
|
||||
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
|
||||
data-flux-column>
|
||||
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Description</div>
|
||||
</th>
|
||||
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
|
||||
data-flux-column>
|
||||
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Grays</div>
|
||||
</th>
|
||||
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
|
||||
data-flux-column>
|
||||
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Colors</div>
|
||||
</th>
|
||||
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
|
||||
data-flux-column>
|
||||
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Actions</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-zinc-800/10 dark:divide-white/20" data-flux-rows>
|
||||
@foreach ($devicePalettes as $devicePalette)
|
||||
<tr data-flux-row>
|
||||
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
|
||||
>
|
||||
<div>
|
||||
<div class="font-medium text-zinc-800 dark:text-white">{{ $devicePalette->description ?? $devicePalette->name }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ $devicePalette->name }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
|
||||
>
|
||||
{{ $devicePalette->grays }}
|
||||
</td>
|
||||
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
|
||||
>
|
||||
@if ($devicePalette->colors)
|
||||
<div class="flex gap-1">
|
||||
@foreach ($devicePalette->colors as $color)
|
||||
<div class="w-4 h-4 rounded border border-zinc-300 dark:border-zinc-600" style="background-color: {{ $color }}"></div>
|
||||
@endforeach
|
||||
<span class="ml-2">({{ count($devicePalette->colors) }})</span>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-zinc-400">Grayscale only</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap font-medium text-zinc-800 dark:text-white"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:button.group>
|
||||
@if ($devicePalette->source === 'api')
|
||||
<flux:modal.trigger name="device-palette-modal">
|
||||
<flux:button wire:click="openDevicePaletteModal('{{ $devicePalette->id }}', true)" icon="eye"
|
||||
iconVariant="outline">
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
<flux:button wire:click="duplicateDevicePalette('{{ $devicePalette->id }}')" icon="document-duplicate"
|
||||
iconVariant="outline">
|
||||
</flux:button>
|
||||
@else
|
||||
<flux:modal.trigger name="device-palette-modal">
|
||||
<flux:button wire:click="openDevicePaletteModal('{{ $devicePalette->id }}')" icon="pencil"
|
||||
iconVariant="outline">
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
<flux:button wire:click="deleteDevicePalette('{{ $devicePalette->id }}')" icon="trash"
|
||||
iconVariant="outline">
|
||||
</flux:button>
|
||||
@endif
|
||||
</flux:button.group>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -121,7 +121,16 @@ new class extends Component {
|
|||
{{--@dump($devices)--}}
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<h2 class="text-2xl font-semibold dark:text-gray-100">Devices</h2>
|
||||
<flux:dropdown>
|
||||
<flux:button icon="chevron-down" variant="ghost"></flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item href="{{ route('device-models.index') }}">Device Models</flux:menu.item>
|
||||
<flux:menu.item href="{{ route('device-palettes.index') }}">Device Palettes</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</div>
|
||||
<flux:modal.trigger name="create-device">
|
||||
<flux:button icon="plus" variant="primary">Add Device</flux:button>
|
||||
</flux:modal.trigger>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ Route::middleware(['auth'])->group(function () {
|
|||
Volt::route('/devices/{device}/logs', 'devices.logs')->name('devices.logs');
|
||||
|
||||
Volt::route('/device-models', 'device-models.index')->name('device-models.index');
|
||||
Volt::route('/device-palettes', 'device-palettes.index')->name('device-palettes.index');
|
||||
|
||||
Volt::route('plugins', 'plugins.index')->name('plugins.index');
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@ uses(RefreshDatabase::class);
|
|||
|
||||
beforeEach(function (): void {
|
||||
DeviceModel::truncate();
|
||||
|
||||
// Mock palettes API to return empty array by default
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response([
|
||||
'data' => [],
|
||||
], 200),
|
||||
]);
|
||||
});
|
||||
|
||||
test('fetch device models job can be dispatched', function (): void {
|
||||
|
|
@ -21,6 +28,7 @@ test('fetch device models job can be dispatched', function (): void {
|
|||
|
||||
test('fetch device models job handles successful api response', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => Http::response([
|
||||
'data' => [
|
||||
[
|
||||
|
|
@ -42,6 +50,10 @@ test('fetch device models job handles successful api response', function (): voi
|
|||
], 200),
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated device models', ['count' => 1]);
|
||||
|
|
@ -67,6 +79,7 @@ test('fetch device models job handles successful api response', function (): voi
|
|||
|
||||
test('fetch device models job handles multiple device models', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => Http::response([
|
||||
'data' => [
|
||||
[
|
||||
|
|
@ -103,6 +116,10 @@ test('fetch device models job handles multiple device models', function (): void
|
|||
], 200),
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated device models', ['count' => 2]);
|
||||
|
|
@ -116,11 +133,16 @@ test('fetch device models job handles multiple device models', function (): void
|
|||
|
||||
test('fetch device models job handles empty data array', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => Http::response([
|
||||
'data' => [],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated device models', ['count' => 0]);
|
||||
|
|
@ -133,11 +155,16 @@ test('fetch device models job handles empty data array', function (): void {
|
|||
|
||||
test('fetch device models job handles missing data field', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => Http::response([
|
||||
'message' => 'No data available',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated device models', ['count' => 0]);
|
||||
|
|
@ -150,11 +177,16 @@ test('fetch device models job handles missing data field', function (): void {
|
|||
|
||||
test('fetch device models job handles non-array data', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => Http::response([
|
||||
'data' => 'invalid-data',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('error')
|
||||
->once()
|
||||
->with('Invalid response format from device models API', Mockery::type('array'));
|
||||
|
|
@ -167,11 +199,16 @@ test('fetch device models job handles non-array data', function (): void {
|
|||
|
||||
test('fetch device models job handles api failure', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => Http::response([
|
||||
'error' => 'Internal Server Error',
|
||||
], 500),
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('error')
|
||||
->once()
|
||||
->with('Failed to fetch device models from API', [
|
||||
|
|
@ -187,11 +224,16 @@ test('fetch device models job handles api failure', function (): void {
|
|||
|
||||
test('fetch device models job handles network exception', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => function (): void {
|
||||
throw new Exception('Network connection failed');
|
||||
},
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('error')
|
||||
->once()
|
||||
->with('Exception occurred while fetching device models', Mockery::type('array'));
|
||||
|
|
@ -204,6 +246,7 @@ test('fetch device models job handles network exception', function (): void {
|
|||
|
||||
test('fetch device models job handles device model with missing name', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => Http::response([
|
||||
'data' => [
|
||||
[
|
||||
|
|
@ -214,6 +257,10 @@ test('fetch device models job handles device model with missing name', function
|
|||
], 200),
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->with('Device model data missing name field', Mockery::type('array'));
|
||||
|
|
@ -230,6 +277,7 @@ test('fetch device models job handles device model with missing name', function
|
|||
|
||||
test('fetch device models job handles device model with partial data', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => Http::response([
|
||||
'data' => [
|
||||
[
|
||||
|
|
@ -240,6 +288,10 @@ test('fetch device models job handles device model with partial data', function
|
|||
], 200),
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated device models', ['count' => 1]);
|
||||
|
|
@ -273,6 +325,7 @@ test('fetch device models job updates existing device model', function (): void
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => Http::response([
|
||||
'data' => [
|
||||
[
|
||||
|
|
@ -294,6 +347,10 @@ test('fetch device models job updates existing device model', function (): void
|
|||
], 200),
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated device models', ['count' => 1]);
|
||||
|
|
@ -311,6 +368,7 @@ test('fetch device models job updates existing device model', function (): void
|
|||
|
||||
test('fetch device models job handles processing exception for individual model', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200),
|
||||
'usetrmnl.com/api/models' => Http::response([
|
||||
'data' => [
|
||||
[
|
||||
|
|
@ -327,6 +385,10 @@ test('fetch device models job handles processing exception for individual model'
|
|||
], 200),
|
||||
]);
|
||||
|
||||
Log::shouldReceive('info')
|
||||
->once()
|
||||
->with('Successfully fetched and updated palettes', ['count' => 0]);
|
||||
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->with('Device model data missing name field', Mockery::type('array'));
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Http;
|
|||
use Livewire\Livewire;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
it('loads newest TRMNL recipes on mount', function () {
|
||||
it('loads newest TRMNL recipes on mount', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/recipes.json*' => Http::response([
|
||||
'data' => [
|
||||
|
|
@ -31,7 +31,7 @@ it('loads newest TRMNL recipes on mount', function () {
|
|||
->assertSee('Installs: 10');
|
||||
});
|
||||
|
||||
it('searches TRMNL recipes when search term is provided', function () {
|
||||
it('searches TRMNL recipes when search term is provided', function (): void {
|
||||
Http::fake([
|
||||
// First call (mount -> newest)
|
||||
'usetrmnl.com/recipes.json?*' => Http::sequence()
|
||||
|
|
@ -71,7 +71,7 @@ it('searches TRMNL recipes when search term is provided', function () {
|
|||
->assertSee('Install');
|
||||
});
|
||||
|
||||
it('installs plugin successfully when user is authenticated', function () {
|
||||
it('installs plugin successfully when user is authenticated', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Http::fake([
|
||||
|
|
@ -100,7 +100,7 @@ it('installs plugin successfully when user is authenticated', function () {
|
|||
->assertSee('Error installing plugin'); // This will fail because we don't have a real zip file
|
||||
});
|
||||
|
||||
it('shows error when user is not authenticated', function () {
|
||||
it('shows error when user is not authenticated', function (): void {
|
||||
Http::fake([
|
||||
'usetrmnl.com/recipes.json*' => Http::response([
|
||||
'data' => [
|
||||
|
|
@ -124,7 +124,7 @@ it('shows error when user is not authenticated', function () {
|
|||
->assertStatus(403); // This will return 403 because user is not authenticated
|
||||
});
|
||||
|
||||
it('shows error when plugin installation fails', function () {
|
||||
it('shows error when plugin installation fails', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Http::fake([
|
||||
|
|
|
|||
575
tests/Feature/Volt/DevicePalettesTest.php
Normal file
575
tests/Feature/Volt/DevicePalettesTest.php
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\DevicePalette;
|
||||
use App\Models\User;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
|
||||
test('device palettes page can be rendered', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$this->get(route('device-palettes.index'))->assertOk();
|
||||
});
|
||||
|
||||
test('component loads all device palettes on mount', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$initialCount = DevicePalette::count();
|
||||
DevicePalette::create(['name' => 'palette-1', 'grays' => 2, 'framework_class' => '']);
|
||||
DevicePalette::create(['name' => 'palette-2', 'grays' => 4, 'framework_class' => '']);
|
||||
DevicePalette::create(['name' => 'palette-3', 'grays' => 16, 'framework_class' => '']);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index');
|
||||
|
||||
$palettes = $component->get('devicePalettes');
|
||||
expect($palettes)->toHaveCount($initialCount + 3);
|
||||
});
|
||||
|
||||
test('can open modal to create new device palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->call('openDevicePaletteModal');
|
||||
|
||||
$component
|
||||
->assertSet('editingDevicePaletteId', null)
|
||||
->assertSet('viewingDevicePaletteId', null)
|
||||
->assertSet('name', null)
|
||||
->assertSet('grays', 2);
|
||||
});
|
||||
|
||||
test('can create a new device palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'test-palette')
|
||||
->set('description', 'Test Palette Description')
|
||||
->set('grays', 16)
|
||||
->set('colors', ['#FF0000', '#00FF00'])
|
||||
->set('framework_class', 'TestFramework')
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasNoErrors();
|
||||
|
||||
expect(DevicePalette::where('name', 'test-palette')->exists())->toBeTrue();
|
||||
|
||||
$palette = DevicePalette::where('name', 'test-palette')->first();
|
||||
expect($palette->description)->toBe('Test Palette Description');
|
||||
expect($palette->grays)->toBe(16);
|
||||
expect($palette->colors)->toBe(['#FF0000', '#00FF00']);
|
||||
expect($palette->framework_class)->toBe('TestFramework');
|
||||
expect($palette->source)->toBe('manual');
|
||||
});
|
||||
|
||||
test('can create a grayscale-only palette without colors', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'grayscale-palette')
|
||||
->set('grays', 256)
|
||||
->set('colors', [])
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasNoErrors();
|
||||
|
||||
$palette = DevicePalette::where('name', 'grayscale-palette')->first();
|
||||
expect($palette->colors)->toBeNull();
|
||||
expect($palette->grays)->toBe(256);
|
||||
});
|
||||
|
||||
test('can open modal to edit existing device palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$palette = DevicePalette::create([
|
||||
'name' => 'existing-palette',
|
||||
'description' => 'Existing Description',
|
||||
'grays' => 4,
|
||||
'colors' => ['#FF0000', '#00FF00'],
|
||||
'framework_class' => 'Framework',
|
||||
'source' => 'manual',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->call('openDevicePaletteModal', $palette->id);
|
||||
|
||||
$component
|
||||
->assertSet('editingDevicePaletteId', $palette->id)
|
||||
->assertSet('name', 'existing-palette')
|
||||
->assertSet('description', 'Existing Description')
|
||||
->assertSet('grays', 4)
|
||||
->assertSet('colors', ['#FF0000', '#00FF00'])
|
||||
->assertSet('framework_class', 'Framework');
|
||||
});
|
||||
|
||||
test('can update an existing device palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$palette = DevicePalette::create([
|
||||
'name' => 'original-palette',
|
||||
'grays' => 2,
|
||||
'framework_class' => '',
|
||||
'source' => 'manual',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->call('openDevicePaletteModal', $palette->id)
|
||||
->set('name', 'updated-palette')
|
||||
->set('description', 'Updated Description')
|
||||
->set('grays', 16)
|
||||
->set('colors', ['#0000FF'])
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasNoErrors();
|
||||
|
||||
$palette->refresh();
|
||||
expect($palette->name)->toBe('updated-palette');
|
||||
expect($palette->description)->toBe('Updated Description');
|
||||
expect($palette->grays)->toBe(16);
|
||||
expect($palette->colors)->toBe(['#0000FF']);
|
||||
});
|
||||
|
||||
test('can delete a device palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$palette = DevicePalette::create([
|
||||
'name' => 'to-delete',
|
||||
'grays' => 2,
|
||||
'framework_class' => '',
|
||||
'source' => 'manual',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->call('deleteDevicePalette', $palette->id);
|
||||
|
||||
expect(DevicePalette::find($palette->id))->toBeNull();
|
||||
$component->assertSet('devicePalettes', function ($palettes) use ($palette) {
|
||||
return $palettes->where('id', $palette->id)->isEmpty();
|
||||
});
|
||||
});
|
||||
|
||||
test('can duplicate a device palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$palette = DevicePalette::create([
|
||||
'name' => 'original-palette',
|
||||
'description' => 'Original Description',
|
||||
'grays' => 4,
|
||||
'colors' => ['#FF0000', '#00FF00'],
|
||||
'framework_class' => 'Framework',
|
||||
'source' => 'manual',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->call('duplicateDevicePalette', $palette->id);
|
||||
|
||||
$component
|
||||
->assertSet('editingDevicePaletteId', null)
|
||||
->assertSet('name', 'original-palette (Copy)')
|
||||
->assertSet('description', 'Original Description')
|
||||
->assertSet('grays', 4)
|
||||
->assertSet('colors', ['#FF0000', '#00FF00'])
|
||||
->assertSet('framework_class', 'Framework');
|
||||
});
|
||||
|
||||
test('can add a color to the colors array', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('colorInput', '#FF0000')
|
||||
->call('addColor');
|
||||
|
||||
$component
|
||||
->assertHasNoErrors()
|
||||
->assertSet('colors', ['#FF0000'])
|
||||
->assertSet('colorInput', '');
|
||||
});
|
||||
|
||||
test('cannot add duplicate colors', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('colors', ['#FF0000'])
|
||||
->set('colorInput', '#FF0000')
|
||||
->call('addColor');
|
||||
|
||||
$component
|
||||
->assertHasNoErrors()
|
||||
->assertSet('colors', ['#FF0000']);
|
||||
});
|
||||
|
||||
test('can add multiple colors', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('colorInput', '#FF0000')
|
||||
->call('addColor')
|
||||
->set('colorInput', '#00FF00')
|
||||
->call('addColor')
|
||||
->set('colorInput', '#0000FF')
|
||||
->call('addColor');
|
||||
|
||||
$component
|
||||
->assertSet('colors', ['#FF0000', '#00FF00', '#0000FF']);
|
||||
});
|
||||
|
||||
test('can remove a color from the colors array', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('colors', ['#FF0000', '#00FF00', '#0000FF'])
|
||||
->call('removeColor', 1);
|
||||
|
||||
$component->assertSet('colors', ['#FF0000', '#0000FF']);
|
||||
});
|
||||
|
||||
test('removing color reindexes array', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('colors', ['#FF0000', '#00FF00', '#0000FF'])
|
||||
->call('removeColor', 0);
|
||||
|
||||
$colors = $component->get('colors');
|
||||
expect($colors)->toBe(['#00FF00', '#0000FF']);
|
||||
expect(array_keys($colors))->toBe([0, 1]);
|
||||
});
|
||||
|
||||
test('can open modal in view-only mode for api-sourced palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$palette = DevicePalette::create([
|
||||
'name' => 'api-palette',
|
||||
'grays' => 2,
|
||||
'framework_class' => '',
|
||||
'source' => 'api',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->call('openDevicePaletteModal', $palette->id, true);
|
||||
|
||||
$component
|
||||
->assertSet('viewingDevicePaletteId', $palette->id)
|
||||
->assertSet('editingDevicePaletteId', null);
|
||||
});
|
||||
|
||||
test('name is required when creating device palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('grays', 16)
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasErrors(['name']);
|
||||
});
|
||||
|
||||
test('name must be unique when creating device palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
DevicePalette::create([
|
||||
'name' => 'existing-name',
|
||||
'grays' => 2,
|
||||
'framework_class' => '',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'existing-name')
|
||||
->set('grays', 16)
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasErrors(['name']);
|
||||
});
|
||||
|
||||
test('name can be same when updating device palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$palette = DevicePalette::create([
|
||||
'name' => 'original-name',
|
||||
'grays' => 2,
|
||||
'framework_class' => '',
|
||||
'source' => 'manual',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->call('openDevicePaletteModal', $palette->id)
|
||||
->set('grays', 16)
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasNoErrors();
|
||||
});
|
||||
|
||||
test('grays is required when creating device palette', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'test-palette')
|
||||
->set('grays', null)
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasErrors(['grays']);
|
||||
});
|
||||
|
||||
test('grays must be at least 1', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'test-palette')
|
||||
->set('grays', 0)
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasErrors(['grays']);
|
||||
});
|
||||
|
||||
test('grays must be at most 256', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'test-palette')
|
||||
->set('grays', 257)
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasErrors(['grays']);
|
||||
});
|
||||
|
||||
test('colors must be valid hex format', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'test-palette')
|
||||
->set('grays', 16)
|
||||
->set('colors', ['invalid-color', '#FF0000'])
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasErrors(['colors.0']);
|
||||
});
|
||||
|
||||
test('color input must be valid hex format when adding color', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('colorInput', 'invalid-color')
|
||||
->call('addColor');
|
||||
|
||||
$component->assertHasErrors(['colorInput']);
|
||||
});
|
||||
|
||||
test('color input accepts valid hex format', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('colorInput', '#FF0000')
|
||||
->call('addColor');
|
||||
|
||||
$component->assertHasNoErrors();
|
||||
});
|
||||
|
||||
test('color input accepts lowercase hex format', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('colorInput', '#ff0000')
|
||||
->call('addColor');
|
||||
|
||||
$component->assertHasNoErrors();
|
||||
});
|
||||
|
||||
test('description can be null', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'test-palette')
|
||||
->set('grays', 16)
|
||||
->set('description', null)
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasNoErrors();
|
||||
|
||||
$palette = DevicePalette::where('name', 'test-palette')->first();
|
||||
expect($palette->description)->toBeNull();
|
||||
});
|
||||
|
||||
test('framework class can be empty string', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'test-palette')
|
||||
->set('grays', 16)
|
||||
->set('framework_class', '')
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasNoErrors();
|
||||
|
||||
$palette = DevicePalette::where('name', 'test-palette')->first();
|
||||
expect($palette->framework_class)->toBe('');
|
||||
});
|
||||
|
||||
test('empty colors array is saved as null', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'test-palette')
|
||||
->set('grays', 16)
|
||||
->set('colors', [])
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component->assertHasNoErrors();
|
||||
|
||||
$palette = DevicePalette::where('name', 'test-palette')->first();
|
||||
expect($palette->colors)->toBeNull();
|
||||
});
|
||||
|
||||
test('component resets form after saving', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'test-palette')
|
||||
->set('description', 'Test Description')
|
||||
->set('grays', 16)
|
||||
->set('colors', ['#FF0000'])
|
||||
->set('framework_class', 'TestFramework')
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$component
|
||||
->assertSet('name', null)
|
||||
->assertSet('description', null)
|
||||
->assertSet('grays', 2)
|
||||
->assertSet('colors', [])
|
||||
->assertSet('framework_class', '')
|
||||
->assertSet('colorInput', '')
|
||||
->assertSet('editingDevicePaletteId', null)
|
||||
->assertSet('viewingDevicePaletteId', null);
|
||||
});
|
||||
|
||||
test('component handles palette with null colors when editing', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$palette = DevicePalette::create([
|
||||
'name' => 'grayscale-palette',
|
||||
'grays' => 2,
|
||||
'colors' => null,
|
||||
'framework_class' => '',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->call('openDevicePaletteModal', $palette->id);
|
||||
|
||||
$component->assertSet('colors', []);
|
||||
});
|
||||
|
||||
test('component handles palette with string colors when editing', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$palette = DevicePalette::create([
|
||||
'name' => 'string-colors-palette',
|
||||
'grays' => 2,
|
||||
'framework_class' => '',
|
||||
]);
|
||||
// Manually set colors as JSON string to simulate edge case
|
||||
$palette->setRawAttributes(array_merge($palette->getAttributes(), [
|
||||
'colors' => json_encode(['#FF0000', '#00FF00']),
|
||||
]));
|
||||
$palette->save();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->call('openDevicePaletteModal', $palette->id);
|
||||
|
||||
$component->assertSet('colors', ['#FF0000', '#00FF00']);
|
||||
});
|
||||
|
||||
test('component refreshes palette list after creating', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$initialCount = DevicePalette::count();
|
||||
DevicePalette::create(['name' => 'palette-1', 'grays' => 2, 'framework_class' => '']);
|
||||
DevicePalette::create(['name' => 'palette-2', 'grays' => 4, 'framework_class' => '']);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->set('name', 'new-palette')
|
||||
->set('grays', 16)
|
||||
->call('saveDevicePalette');
|
||||
|
||||
$palettes = $component->get('devicePalettes');
|
||||
expect($palettes)->toHaveCount($initialCount + 3);
|
||||
expect(DevicePalette::count())->toBe($initialCount + 3);
|
||||
});
|
||||
|
||||
test('component refreshes palette list after deleting', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$initialCount = DevicePalette::count();
|
||||
$palette1 = DevicePalette::create([
|
||||
'name' => 'palette-1',
|
||||
'grays' => 2,
|
||||
'framework_class' => '',
|
||||
'source' => 'manual',
|
||||
]);
|
||||
$palette2 = DevicePalette::create([
|
||||
'name' => 'palette-2',
|
||||
'grays' => 2,
|
||||
'framework_class' => '',
|
||||
'source' => 'manual',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('device-palettes.index')
|
||||
->call('deleteDevicePalette', $palette1->id);
|
||||
|
||||
$palettes = $component->get('devicePalettes');
|
||||
expect($palettes)->toHaveCount($initialCount + 1);
|
||||
expect(DevicePalette::count())->toBe($initialCount + 1);
|
||||
});
|
||||
|
|
@ -77,7 +77,7 @@ test('l_date handles null locale parameter', function (): void {
|
|||
$filter = new Localization();
|
||||
$date = '2025-01-11';
|
||||
|
||||
$result = $filter->l_date($date, 'Y-m-d', null);
|
||||
$result = $filter->l_date($date, 'Y-m-d');
|
||||
|
||||
// Should work the same as default
|
||||
expect($result)->toContain('2025');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue