mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 23:18:10 +00:00
feat(#150): add trmnlp settings modal
This commit is contained in:
parent
a86315c5c7
commit
0d6079db8b
4 changed files with 263 additions and 2 deletions
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Find and handle duplicate (user_id, trmnlp_id) combinations
|
||||||
|
$duplicates = DB::table('plugins')
|
||||||
|
->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']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -478,7 +478,6 @@ HTML;
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
</flux:menu>
|
</flux:menu>
|
||||||
</flux:dropdown>
|
</flux:dropdown>
|
||||||
|
|
||||||
</flux:button.group>
|
</flux:button.group>
|
||||||
<flux:button.group>
|
<flux:button.group>
|
||||||
<flux:modal.trigger name="add-to-playlist">
|
<flux:modal.trigger name="add-to-playlist">
|
||||||
|
|
@ -488,6 +487,10 @@ HTML;
|
||||||
<flux:dropdown>
|
<flux:dropdown>
|
||||||
<flux:button icon="chevron-down" variant="primary"></flux:button>
|
<flux:button icon="chevron-down" variant="primary"></flux:button>
|
||||||
<flux:menu>
|
<flux:menu>
|
||||||
|
<flux:modal.trigger name="trmnlp-settings">
|
||||||
|
<flux:menu.item icon="cog">Recipe Settings</flux:menu.item>
|
||||||
|
</flux:modal.trigger>
|
||||||
|
<flux:menu.separator />
|
||||||
<flux:menu.item icon="document-duplicate" wire:click="duplicatePlugin">Duplicate Plugin</flux:menu.item>
|
<flux:menu.item icon="document-duplicate" wire:click="duplicatePlugin">Duplicate Plugin</flux:menu.item>
|
||||||
<flux:modal.trigger name="delete-plugin">
|
<flux:modal.trigger name="delete-plugin">
|
||||||
<flux:menu.item icon="trash" variant="danger">Delete Plugin</flux:menu.item>
|
<flux:menu.item icon="trash" variant="danger">Delete Plugin</flux:menu.item>
|
||||||
|
|
@ -646,6 +649,8 @@ HTML;
|
||||||
</div>
|
</div>
|
||||||
</flux:modal>
|
</flux:modal>
|
||||||
|
|
||||||
|
<livewire:plugins.recipes.settings :plugin="$plugin" />
|
||||||
|
|
||||||
<livewire:plugins.config-modal :plugin="$plugin" />
|
<livewire:plugins.config-modal :plugin="$plugin" />
|
||||||
|
|
||||||
<div class="mt-5 mb-5">
|
<div class="mt-5 mb-5">
|
||||||
|
|
@ -734,7 +739,7 @@ HTML;
|
||||||
@endif
|
@endif
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<flux:modal.trigger name="configuration-modal">
|
<flux:modal.trigger name="configuration-modal">
|
||||||
<flux:button icon="cog" class="block mt-1 w-full">Configuration</flux:button>
|
<flux:button icon="variable" class="block mt-1 w-full">Configuration Fields</flux:button>
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
84
resources/views/livewire/plugins/recipes/settings.blade.php
Normal file
84
resources/views/livewire/plugins/recipes/settings.blade.php
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Plugin;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This component contains the TRMNL Plugin Settings modal
|
||||||
|
*/
|
||||||
|
new class extends Component {
|
||||||
|
public Plugin $plugin;
|
||||||
|
public string|null $trmnlp_id = null;
|
||||||
|
public string|null $uuid = null;
|
||||||
|
|
||||||
|
public int $resetIndex = 0;
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->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();
|
||||||
|
}
|
||||||
|
};?>
|
||||||
|
|
||||||
|
<flux:modal name="trmnlp-settings" class="min-w-[400px] space-y-6">
|
||||||
|
<div wire:key="trmnlp-settings-form-{{ $resetIndex }}" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="lg">Recipe Settings</flux:heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form wire:submit="saveTrmnlpId">
|
||||||
|
<div class="grid gap-6">
|
||||||
|
{{-- <flux:input label="UUID" wire:model="uuid" readonly copyable /> --}}
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>TRMNLP Recipe ID</flux:label>
|
||||||
|
<flux:input
|
||||||
|
wire:model="trmnlp_id"
|
||||||
|
placeholder="TRMNL Recipe ID"
|
||||||
|
/>
|
||||||
|
<flux:error name="trmnlp_id" />
|
||||||
|
<flux:description>Recipe ID in the TRMNL Recipe Catalog. If set, it can be used with <code>trmnlp</code>. </flux:description>
|
||||||
|
</flux:field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<flux:spacer/>
|
||||||
|
<flux:modal.close>
|
||||||
|
<flux:button variant="ghost">Cancel</flux:button>
|
||||||
|
</flux:modal.close>
|
||||||
|
<flux:button type="submit" variant="primary">Save</flux:button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</flux:modal>
|
||||||
112
tests/Feature/Livewire/Plugins/RecipeSettingsTest.php
Normal file
112
tests/Feature/Livewire/Plugins/RecipeSettingsTest.php
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\Plugin;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('recipe settings can save trmnlp_id', function (): void {
|
||||||
|
$user = User::factory()->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();
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue