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 Flux\Flux;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Attributes\Url;
|
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
|
public function render(): View
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
<flux:table.column>Words</flux:table.column>
|
<flux:table.column>Words</flux:table.column>
|
||||||
<flux:table.column>Status</flux:table.column>
|
<flux:table.column>Status</flux:table.column>
|
||||||
<flux:table.column>Uploaded</flux:table.column>
|
<flux:table.column>Uploaded</flux:table.column>
|
||||||
|
<flux:table.column class="w-24"></flux:table.column>
|
||||||
</flux:table.columns>
|
</flux:table.columns>
|
||||||
|
|
||||||
<flux:table.rows>
|
<flux:table.rows>
|
||||||
@@ -152,6 +153,42 @@
|
|||||||
<flux:table.cell>
|
<flux:table.cell>
|
||||||
{{ $recording->created_at?->format('Y-m-d H:i') }}
|
{{ $recording->created_at?->format('Y-m-d H:i') }}
|
||||||
</flux:table.cell>
|
</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>
|
</flux:table.row>
|
||||||
@endforeach
|
@endforeach
|
||||||
</flux:table.rows>
|
</flux:table.rows>
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ use App\Jobs\TranscribeRecording;
|
|||||||
use App\Livewire\Recordings\Index;
|
use App\Livewire\Recordings\Index;
|
||||||
use App\Models\Recording;
|
use App\Models\Recording;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Bus;
|
use Illuminate\Support\Facades\Bus;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -87,4 +89,60 @@ class IndexTest extends TestCase
|
|||||||
|
|
||||||
Bus::assertDispatched(TranscribeRecording::class, 1);
|
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