Major features:
- Switch from slug-based to ID-based routing (/documents/123)
- Enable title editing with automatic slug/path regeneration
- Auto-generate folder structure from title slashes (e.g., Laravel/Livewire/Components)
- Persist sidebar folder open/close state using localStorage
- Remove slug unique constraint (ID routing makes it unnecessary)
- Implement recursive tree view with multi-level folder support
Architecture changes:
- DocumentService: Add generatePathAndSlug() for title-based path generation
- Routes: Change from {document:slug} to {document} for ID binding
- SidebarTree: Extract recursive rendering to partials/tree-item.blade.php
- Database: Remove unique constraint from documents.slug column
UI improvements:
- Display only last path component in sidebar (Components vs Laravel/Livewire/Components)
- Folder state persists across page navigation via localStorage
- Title field accepts slashes for folder organization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
108 lines
3.1 KiB
PHP
108 lines
3.1 KiB
PHP
<?php
|
||
|
||
namespace App\Livewire;
|
||
|
||
use App\Models\Document;
|
||
use App\Services\DocumentService;
|
||
use Livewire\Component;
|
||
use Livewire\Attributes\Computed;
|
||
|
||
class QuickSwitcher extends Component
|
||
{
|
||
public $search = '';
|
||
public $selectedIndex = 0;
|
||
|
||
#[Computed]
|
||
public function results()
|
||
{
|
||
if (empty($this->search)) {
|
||
return Document::select('id', 'title', 'slug', 'path', 'updated_at')
|
||
->orderBy('updated_at', 'desc')
|
||
->limit(10)
|
||
->get()
|
||
->map(fn($doc) => [
|
||
'id' => $doc->id,
|
||
'title' => $doc->title,
|
||
'slug' => $doc->slug,
|
||
'directory' => dirname($doc->path),
|
||
])
|
||
->toArray();
|
||
}
|
||
|
||
// FULLTEXT検索を使用(日本語対応)
|
||
$results = Document::select('id', 'title', 'slug', 'path', 'updated_at')
|
||
->whereRaw('MATCH(title, content) AGAINST(? IN BOOLEAN MODE)', [$this->search])
|
||
->orderBy('updated_at', 'desc')
|
||
->limit(10)
|
||
->get()
|
||
->map(fn($doc) => [
|
||
'id' => $doc->id,
|
||
'title' => $doc->title,
|
||
'slug' => $doc->slug,
|
||
'directory' => dirname($doc->path),
|
||
])
|
||
->toArray();
|
||
|
||
// FULLTEXT検索で結果がない場合は LIKE 検索にフォールバック
|
||
if (empty($results)) {
|
||
$results = Document::select('id', 'title', 'slug', 'path', 'updated_at')
|
||
->where(function($query) {
|
||
$query->where('title', 'like', '%' . $this->search . '%')
|
||
->orWhere('content', 'like', '%' . $this->search . '%');
|
||
})
|
||
->orderBy('updated_at', 'desc')
|
||
->limit(10)
|
||
->get()
|
||
->map(fn($doc) => [
|
||
'id' => $doc->id,
|
||
'title' => $doc->title,
|
||
'slug' => $doc->slug,
|
||
'directory' => dirname($doc->path),
|
||
])
|
||
->toArray();
|
||
}
|
||
|
||
return $results;
|
||
}
|
||
|
||
public function updated($propertyName)
|
||
{
|
||
if ($propertyName === 'search') {
|
||
$this->selectedIndex = 0;
|
||
}
|
||
}
|
||
|
||
public function selectNext()
|
||
{
|
||
$results = $this->results;
|
||
if ($this->selectedIndex < count($results) - 1) {
|
||
$this->selectedIndex++;
|
||
}
|
||
}
|
||
|
||
public function selectPrevious()
|
||
{
|
||
if ($this->selectedIndex > 0) {
|
||
$this->selectedIndex--;
|
||
}
|
||
}
|
||
|
||
public function selectDocument()
|
||
{
|
||
$results = $this->results;
|
||
if (isset($results[$this->selectedIndex])) {
|
||
$document = $results[$this->selectedIndex];
|
||
|
||
// id が存在することを確認
|
||
if (!empty($document['id'])) {
|
||
return $this->redirect(route('documents.show', $document['id']));
|
||
}
|
||
}
|
||
}
|
||
|
||
public function render()
|
||
{
|
||
return view('livewire.quick-switcher');
|
||
}
|
||
}
|