6e7f8566ef
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');
|
|
}
|
|
}
|