Add delete button with confirm modal on each recordings list row.
This commit is contained in:
@@ -6,6 +6,7 @@ use App\Models\Recording;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Attributes\Url;
|
||||
@@ -56,6 +57,18 @@ class Index extends Component
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(int $recordingId): void
|
||||
{
|
||||
$recording = Auth::user()->recordings()->findOrFail($recordingId);
|
||||
|
||||
Gate::authorize('delete', $recording);
|
||||
|
||||
$recording->deleteFile();
|
||||
$recording->delete();
|
||||
|
||||
Flux::toast(text: 'Recording deleted.', variant: 'success');
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
<flux:table.column>Words</flux:table.column>
|
||||
<flux:table.column>Status</flux:table.column>
|
||||
<flux:table.column>Uploaded</flux:table.column>
|
||||
<flux:table.column class="w-24"></flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@@ -152,6 +153,42 @@
|
||||
<flux:table.cell>
|
||||
{{ $recording->created_at?->format('Y-m-d H:i') }}
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<flux:modal.trigger name="delete-recording-{{ $recording->id }}">
|
||||
<flux:button
|
||||
type="button"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
square
|
||||
aria-label="Delete {{ $recording->title }}"
|
||||
>
|
||||
<flux:icon.trash variant="micro" />
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
|
||||
<flux:modal name="delete-recording-{{ $recording->id }}" class="max-w-md">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">Delete recording?</flux:heading>
|
||||
<flux:text class="mt-2">
|
||||
This permanently removes “{{ $recording->title }}” and its audio file.
|
||||
</flux:text>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<flux:modal.close>
|
||||
<flux:button variant="ghost">Cancel</flux:button>
|
||||
</flux:modal.close>
|
||||
<flux:button
|
||||
type="button"
|
||||
variant="danger"
|
||||
wire:click="delete({{ $recording->id }})"
|
||||
>
|
||||
Delete
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforeach
|
||||
</flux:table.rows>
|
||||
|
||||
@@ -6,8 +6,10 @@ use App\Jobs\TranscribeRecording;
|
||||
use App\Livewire\Recordings\Index;
|
||||
use App\Models\Recording;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -87,4 +89,60 @@ class IndexTest extends TestCase
|
||||
|
||||
Bus::assertDispatched(TranscribeRecording::class, 1);
|
||||
}
|
||||
|
||||
public function test_user_can_delete_recording_from_index(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
Storage::disk('local')->put('recordings/delete-me.mp3', 'bytes');
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'title' => 'Delete from list',
|
||||
'original_filename' => 'delete-me.mp3',
|
||||
'file_path' => 'recordings/delete-me.mp3',
|
||||
'file_size_bytes' => 5,
|
||||
'transcription_status' => 'done',
|
||||
]);
|
||||
|
||||
Livewire::test(Index::class)
|
||||
->assertSee('Delete from list')
|
||||
->call('delete', $recording->id)
|
||||
->assertDontSee('Delete from list');
|
||||
|
||||
$this->assertDatabaseMissing('recordings', ['id' => $recording->id]);
|
||||
Storage::disk('local')->assertMissing('recordings/delete-me.mp3');
|
||||
}
|
||||
|
||||
public function test_user_cannot_delete_another_users_recording_from_index(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
|
||||
$owner = User::factory()->create();
|
||||
$intruder = User::factory()->create();
|
||||
|
||||
$recording = Recording::query()->create([
|
||||
'user_id' => $owner->id,
|
||||
'title' => 'Keep me',
|
||||
'original_filename' => 'keep.mp3',
|
||||
'file_path' => 'recordings/keep.mp3',
|
||||
'file_size_bytes' => 10,
|
||||
'transcription_status' => 'done',
|
||||
]);
|
||||
|
||||
$this->actingAs($intruder);
|
||||
|
||||
try {
|
||||
Livewire::test(Index::class)
|
||||
->call('delete', $recording->id);
|
||||
|
||||
$this->fail('Expected deleting another user\'s recording to fail.');
|
||||
} catch (ModelNotFoundException) {
|
||||
// Owned-query findOrFail hides other users' recordings.
|
||||
}
|
||||
|
||||
$this->assertDatabaseHas('recordings', ['id' => $recording->id]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user