From 0d6079db8bf475702a579228ea5277d2001c4e6d Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Fri, 9 Jan 2026 20:16:33 +0100 Subject: [PATCH] feat(#150): add trmnlp settings modal --- ...make_trmnlp_id_unique_in_plugins_table.php | 60 ++++++++++ .../views/livewire/plugins/recipe.blade.php | 9 +- .../plugins/recipes/settings.blade.php | 84 +++++++++++++ .../Livewire/Plugins/RecipeSettingsTest.php | 112 ++++++++++++++++++ 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php create mode 100644 resources/views/livewire/plugins/recipes/settings.blade.php create mode 100644 tests/Feature/Livewire/Plugins/RecipeSettingsTest.php diff --git a/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php b/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php new file mode 100644 index 0000000..9769505 --- /dev/null +++ b/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php @@ -0,0 +1,60 @@ +select('user_id', 'trmnlp_id', DB::raw('COUNT(*) as count')) + ->whereNotNull('trmnlp_id') + ->groupBy('user_id', 'trmnlp_id') + ->having('count', '>', 1) + ->get(); + + // For each duplicate combination, keep the first one (by id) and set others to null + foreach ($duplicates as $duplicate) { + $plugins = DB::table('plugins') + ->where('user_id', $duplicate->user_id) + ->where('trmnlp_id', $duplicate->trmnlp_id) + ->orderBy('id') + ->get(); + + // Keep the first one, set the rest to null + $keepFirst = true; + foreach ($plugins as $plugin) { + if ($keepFirst) { + $keepFirst = false; + + continue; + } + + DB::table('plugins') + ->where('id', $plugin->id) + ->update(['trmnlp_id' => null]); + } + } + + Schema::table('plugins', function (Blueprint $table) { + $table->unique(['user_id', 'trmnlp_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropUnique(['user_id', 'trmnlp_id']); + }); + } +}; diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php index 19ee3a7..c9a1442 100644 --- a/resources/views/livewire/plugins/recipe.blade.php +++ b/resources/views/livewire/plugins/recipe.blade.php @@ -478,7 +478,6 @@ HTML; - @@ -488,6 +487,10 @@ HTML; + + Recipe Settings + + Duplicate Plugin Delete Plugin @@ -646,6 +649,8 @@ HTML; + +
@@ -734,7 +739,7 @@ HTML; @endif
- Configuration + Configuration Fields
@endif diff --git a/resources/views/livewire/plugins/recipes/settings.blade.php b/resources/views/livewire/plugins/recipes/settings.blade.php new file mode 100644 index 0000000..cb8028f --- /dev/null +++ b/resources/views/livewire/plugins/recipes/settings.blade.php @@ -0,0 +1,84 @@ +loadData(); + } + + public function loadData(): void + { + $this->resetErrorBag(); + // Reload data + $this->plugin = $this->plugin->fresh(); + $this->trmnlp_id = $this->plugin->trmnlp_id; + $this->uuid = $this->plugin->uuid; + } + + public function saveTrmnlpId(): void + { + abort_unless(auth()->user()->plugins->contains($this->plugin), 403); + + $this->validate([ + 'trmnlp_id' => [ + 'nullable', + 'string', + 'max:255', + Rule::unique('plugins', 'trmnlp_id') + ->where('user_id', auth()->id()) + ->ignore($this->plugin->id), + ], + ]); + + $this->plugin->update([ + 'trmnlp_id' => empty($this->trmnlp_id) ? null : $this->trmnlp_id, + ]); + + //$this->loadData(); // Reload to ensure we have the latest data + Flux::modal('trmnlp-settings')->close(); + } +};?> + + +
+
+ Recipe Settings +
+ +
+
+ {{-- --}} + + TRMNLP Recipe ID + + + Recipe ID in the TRMNL Recipe Catalog. If set, it can be used with trmnlp. + +
+ +
+ + + Cancel + + Save +
+
+
+
diff --git a/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php b/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php new file mode 100644 index 0000000..a04815f --- /dev/null +++ b/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php @@ -0,0 +1,112 @@ +create(); + $this->actingAs($user); + + $plugin = Plugin::factory()->create([ + 'user_id' => $user->id, + 'trmnlp_id' => null, + ]); + + $trmnlpId = (string) Str::uuid(); + + Volt::test('plugins.recipes.settings', ['plugin' => $plugin]) + ->set('trmnlp_id', $trmnlpId) + ->call('saveTrmnlpId') + ->assertHasNoErrors(); + + expect($plugin->fresh()->trmnlp_id)->toBe($trmnlpId); +}); + +test('recipe settings validates trmnlp_id is unique per user', function (): void { + $user = User::factory()->create(); + $this->actingAs($user); + + $existingPlugin = Plugin::factory()->create([ + 'user_id' => $user->id, + 'trmnlp_id' => 'existing-id-123', + ]); + + $newPlugin = Plugin::factory()->create([ + 'user_id' => $user->id, + 'trmnlp_id' => null, + ]); + + Volt::test('plugins.recipes.settings', ['plugin' => $newPlugin]) + ->set('trmnlp_id', 'existing-id-123') + ->call('saveTrmnlpId') + ->assertHasErrors(['trmnlp_id' => 'unique']); + + expect($newPlugin->fresh()->trmnlp_id)->toBeNull(); +}); + +test('recipe settings allows same trmnlp_id for different users', function (): void { + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + + $plugin1 = Plugin::factory()->create([ + 'user_id' => $user1->id, + 'trmnlp_id' => 'shared-id-123', + ]); + + $plugin2 = Plugin::factory()->create([ + 'user_id' => $user2->id, + 'trmnlp_id' => null, + ]); + + $this->actingAs($user2); + + Volt::test('plugins.recipes.settings', ['plugin' => $plugin2]) + ->set('trmnlp_id', 'shared-id-123') + ->call('saveTrmnlpId') + ->assertHasNoErrors(); + + expect($plugin2->fresh()->trmnlp_id)->toBe('shared-id-123'); +}); + +test('recipe settings allows same trmnlp_id for the same plugin', function (): void { + $user = User::factory()->create(); + $this->actingAs($user); + + $trmnlpId = (string) Str::uuid(); + + $plugin = Plugin::factory()->create([ + 'user_id' => $user->id, + 'trmnlp_id' => $trmnlpId, + ]); + + Volt::test('plugins.recipes.settings', ['plugin' => $plugin]) + ->set('trmnlp_id', $trmnlpId) + ->call('saveTrmnlpId') + ->assertHasNoErrors(); + + expect($plugin->fresh()->trmnlp_id)->toBe($trmnlpId); +}); + +test('recipe settings can clear trmnlp_id', function (): void { + $user = User::factory()->create(); + $this->actingAs($user); + + $plugin = Plugin::factory()->create([ + 'user_id' => $user->id, + 'trmnlp_id' => 'some-id', + ]); + + Volt::test('plugins.recipes.settings', ['plugin' => $plugin]) + ->set('trmnlp_id', '') + ->call('saveTrmnlpId') + ->assertHasNoErrors(); + + expect($plugin->fresh()->trmnlp_id)->toBeNull(); +});