From fdfc3bd34160105f761c69c51e351b08d2e144a7 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 29 Oct 2025 10:47:27 +0100 Subject: [PATCH] feat: add installation function --- .../views/livewire/catalog/trmnl.blade.php | 56 ++++++++++--- tests/Feature/Volt/CatalogTrmnlTest.php | 79 +++++++++++++++++++ 2 files changed, 122 insertions(+), 13 deletions(-) diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index 81cab20..6f04ca4 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -4,12 +4,14 @@ use Livewire\Volt\Component; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Cache; +use App\Services\PluginImportService; +use Illuminate\Support\Facades\Auth; new class extends Component { public array $recipes = []; public string $search = ''; - public string $error = ''; public bool $isSearching = false; + public string $installingPlugin = ''; public function mount(): void { @@ -18,7 +20,6 @@ new class extends Component { private function loadNewest(): void { - $this->error = ''; try { $this->recipes = Cache::remember('trmnl_recipes_newest', 300, function () { $response = Http::get('https://usetrmnl.com/recipes.json', [ @@ -36,13 +37,11 @@ new class extends Component { } catch (\Throwable $e) { Log::error('TRMNL catalog load error: ' . $e->getMessage()); $this->recipes = []; - $this->error = 'Failed to load the TRMNL catalog. Please try again later.'; } } private function searchRecipes(string $term): void { - $this->error = ''; $this->isSearching = true; try { $cacheKey = 'trmnl_recipes_search_' . md5($term); @@ -63,7 +62,6 @@ new class extends Component { } catch (\Throwable $e) { Log::error('TRMNL catalog search error: ' . $e->getMessage()); $this->recipes = []; - $this->error = 'Search failed. Please try again later.'; } finally { $this->isSearching = false; } @@ -85,6 +83,27 @@ new class extends Component { $this->searchRecipes($term); } + public function installPlugin(string $recipeId, PluginImportService $pluginImportService): void + { + abort_unless(auth()->user() !== null, 403); + + $this->installingPlugin = $recipeId; + + try { + $zipUrl = "https://usetrmnl.com/api/plugin_settings/{$recipeId}/archive"; + $plugin = $pluginImportService->importFromUrl($zipUrl, auth()->user()); + + $this->dispatch('plugin-installed'); + Flux::modal('import-from-trmnl-catalog')->close(); + + } catch (\Exception $e) { + Log::error('Plugin installation failed: ' . $e->getMessage()); + $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); + } finally { + $this->installingPlugin = ''; + } + } + /** * @param array> $items * @return array> @@ -124,9 +143,9 @@ new class extends Component { Newest - @if($error) - - @endif + @error('installation') + + @enderror @if(empty($recipes))
@@ -170,11 +189,22 @@ new class extends Component { @endif
- - - Install - - + @if($recipe['id']) + @if($installingPlugin === $recipe['id']) + + + + @else + + Install + + @endif + @endif @if($recipe['detail_url']) assertSee('Weather Search Result') ->assertSee('Install'); }); + +it('installs plugin successfully when user is authenticated', function () { + $user = User::factory()->create(); + + Http::fake([ + 'usetrmnl.com/recipes.json*' => Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + 'usetrmnl.com/api/plugin_settings/123/archive*' => Http::response('fake zip content', 200), + ]); + + $this->actingAs($user); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->call('installPlugin', '123') + ->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 () { + Http::fake([ + 'usetrmnl.com/recipes.json*' => Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + ]); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->call('installPlugin', '123') + ->assertStatus(403); // This will return 403 because user is not authenticated +}); + +it('shows error when plugin installation fails', function () { + $user = User::factory()->create(); + + Http::fake([ + 'usetrmnl.com/recipes.json*' => Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + 'usetrmnl.com/api/plugin_settings/123/archive*' => Http::response('invalid zip content', 200), + ]); + + $this->actingAs($user); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->call('installPlugin', '123') + ->assertSee('Error installing plugin'); // This will fail because the zip content is invalid +});