mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-14 07:27:47 +00:00
feat: add installation function
This commit is contained in:
parent
73eabe8262
commit
a7a541da42
2 changed files with 122 additions and 13 deletions
|
|
@ -4,12 +4,14 @@ use Livewire\Volt\Component;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use App\Services\PluginImportService;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
new class extends Component {
|
new class extends Component {
|
||||||
public array $recipes = [];
|
public array $recipes = [];
|
||||||
public string $search = '';
|
public string $search = '';
|
||||||
public string $error = '';
|
|
||||||
public bool $isSearching = false;
|
public bool $isSearching = false;
|
||||||
|
public string $installingPlugin = '';
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
|
|
@ -18,7 +20,6 @@ new class extends Component {
|
||||||
|
|
||||||
private function loadNewest(): void
|
private function loadNewest(): void
|
||||||
{
|
{
|
||||||
$this->error = '';
|
|
||||||
try {
|
try {
|
||||||
$this->recipes = Cache::remember('trmnl_recipes_newest', 300, function () {
|
$this->recipes = Cache::remember('trmnl_recipes_newest', 300, function () {
|
||||||
$response = Http::get('https://usetrmnl.com/recipes.json', [
|
$response = Http::get('https://usetrmnl.com/recipes.json', [
|
||||||
|
|
@ -36,13 +37,11 @@ new class extends Component {
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::error('TRMNL catalog load error: ' . $e->getMessage());
|
Log::error('TRMNL catalog load error: ' . $e->getMessage());
|
||||||
$this->recipes = [];
|
$this->recipes = [];
|
||||||
$this->error = 'Failed to load the TRMNL catalog. Please try again later.';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function searchRecipes(string $term): void
|
private function searchRecipes(string $term): void
|
||||||
{
|
{
|
||||||
$this->error = '';
|
|
||||||
$this->isSearching = true;
|
$this->isSearching = true;
|
||||||
try {
|
try {
|
||||||
$cacheKey = 'trmnl_recipes_search_' . md5($term);
|
$cacheKey = 'trmnl_recipes_search_' . md5($term);
|
||||||
|
|
@ -63,7 +62,6 @@ new class extends Component {
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::error('TRMNL catalog search error: ' . $e->getMessage());
|
Log::error('TRMNL catalog search error: ' . $e->getMessage());
|
||||||
$this->recipes = [];
|
$this->recipes = [];
|
||||||
$this->error = 'Search failed. Please try again later.';
|
|
||||||
} finally {
|
} finally {
|
||||||
$this->isSearching = false;
|
$this->isSearching = false;
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +83,27 @@ new class extends Component {
|
||||||
$this->searchRecipes($term);
|
$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<int, array<string, mixed>> $items
|
* @param array<int, array<string, mixed>> $items
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
|
|
@ -124,9 +143,9 @@ new class extends Component {
|
||||||
<flux:badge color="gray">Newest</flux:badge>
|
<flux:badge color="gray">Newest</flux:badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($error)
|
@error('installation')
|
||||||
<flux:callout variant="danger" icon="x-circle" heading="{{ $error }}" />
|
<flux:callout variant="danger" icon="x-circle" heading="{{$message}}" />
|
||||||
@endif
|
@enderror
|
||||||
|
|
||||||
@if(empty($recipes))
|
@if(empty($recipes))
|
||||||
<div class="text-center py-8">
|
<div class="text-center py-8">
|
||||||
|
|
@ -170,11 +189,22 @@ new class extends Component {
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="mt-4 flex items-center space-x-3">
|
<div class="mt-4 flex items-center space-x-3">
|
||||||
<flux:tooltip text="Installation via cloud coming soon">
|
@if($recipe['id'])
|
||||||
<flux:button disabled variant="primary">
|
@if($installingPlugin === $recipe['id'])
|
||||||
Install
|
<flux:button
|
||||||
</flux:button>
|
wire:click="installPlugin('{{ $recipe['id'] }}')"
|
||||||
</flux:tooltip>
|
variant="primary"
|
||||||
|
disabled>
|
||||||
|
<flux:icon name="arrow-path" class="w-4 h-4 animate-spin" />
|
||||||
|
</flux:button>
|
||||||
|
@else
|
||||||
|
<flux:button
|
||||||
|
wire:click="installPlugin('{{ $recipe['id'] }}')"
|
||||||
|
variant="primary">
|
||||||
|
Install
|
||||||
|
</flux:button>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
@if($recipe['detail_url'])
|
@if($recipe['detail_url'])
|
||||||
<flux:button
|
<flux:button
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\PluginImportService;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Livewire\Volt\Volt;
|
use Livewire\Volt\Volt;
|
||||||
|
use Mockery\MockInterface;
|
||||||
|
|
||||||
it('loads newest TRMNL recipes on mount', function () {
|
it('loads newest TRMNL recipes on mount', function () {
|
||||||
Http::fake([
|
Http::fake([
|
||||||
|
|
@ -64,3 +67,79 @@ it('searches TRMNL recipes when search term is provided', function () {
|
||||||
->assertSee('Weather Search Result')
|
->assertSee('Weather Search Result')
|
||||||
->assertSee('Install');
|
->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
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue