Add locale tabs to DocumentEditor
Editor accepts a locale URL parameter, loads the corresponding translation (or empty form for new locales), and exposes addTranslation/setDefaultLocale/deleteTranslation actions. Tab bar shows existing locales with default-locale star and a + dropdown for missing locales. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,30 +2,44 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Http\Middleware\SetLocale;
|
||||
use App\Models\Document;
|
||||
use App\Services\DocumentService;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class DocumentEditor extends Component
|
||||
{
|
||||
public ?Document $document = null;
|
||||
public $title = '';
|
||||
public $content = '';
|
||||
public $directory = '';
|
||||
public $isEditMode = false;
|
||||
public string $title = '';
|
||||
public string $content = '';
|
||||
public string $editingLocale = '';
|
||||
public bool $isEditMode = false;
|
||||
public bool $isNewLocale = false;
|
||||
public array $availableLocales = [];
|
||||
|
||||
public function mount(?Document $document = null)
|
||||
public function mount(?Document $document = null, ?string $locale = null)
|
||||
{
|
||||
if ($document) {
|
||||
$this->authorize('update', $document);
|
||||
|
||||
$this->document = $document;
|
||||
$this->title = $document->title;
|
||||
$this->content = $document->content;
|
||||
$this->directory = $document->directory;
|
||||
$this->document = $document->load('translations');
|
||||
$this->isEditMode = true;
|
||||
$this->availableLocales = $document->availableLocales();
|
||||
$this->editingLocale = $locale ?: ($document->default_locale ?? App::getLocale());
|
||||
|
||||
$translation = $document->translations->firstWhere('locale', $this->editingLocale);
|
||||
if ($translation) {
|
||||
$this->title = $translation->title;
|
||||
$this->content = $translation->content;
|
||||
$this->isNewLocale = false;
|
||||
} else {
|
||||
$this->title = '';
|
||||
$this->content = '';
|
||||
$this->isNewLocale = true;
|
||||
}
|
||||
} else {
|
||||
$this->editingLocale = App::getLocale();
|
||||
$titleParam = request()->query('title');
|
||||
if ($titleParam) {
|
||||
$this->title = $titleParam;
|
||||
@@ -35,53 +49,96 @@ public function mount(?Document $document = null)
|
||||
|
||||
public function save(DocumentService $documentService)
|
||||
{
|
||||
$this->validate([
|
||||
$validated = $this->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'content' => 'required|string',
|
||||
'editingLocale' => ['required', 'string', 'in:' . implode(',', array_keys(SetLocale::SUPPORTED_LOCALES))],
|
||||
]);
|
||||
|
||||
try {
|
||||
if ($this->isEditMode && $this->document) {
|
||||
$this->authorize('update', $this->document);
|
||||
|
||||
$this->document = $documentService->updateDocument(
|
||||
$this->document,
|
||||
$this->title,
|
||||
$this->content,
|
||||
Auth::id()
|
||||
);
|
||||
if ($this->isNewLocale) {
|
||||
$documentService->addTranslation(
|
||||
$this->document,
|
||||
$this->editingLocale,
|
||||
$this->title,
|
||||
$this->content,
|
||||
Auth::id(),
|
||||
);
|
||||
$this->document->refresh()->load('translations');
|
||||
} else {
|
||||
$this->document = $documentService->updateDocument(
|
||||
$this->document,
|
||||
$this->title,
|
||||
$this->content,
|
||||
Auth::id(),
|
||||
$this->editingLocale,
|
||||
);
|
||||
}
|
||||
|
||||
session()->flash('message', 'Document updated successfully!');
|
||||
session()->flash('message', __('messages.documents.update_success'));
|
||||
return $this->redirect(route('documents.show', $this->document));
|
||||
} else {
|
||||
$this->document = $documentService->createDocument(
|
||||
$this->title,
|
||||
$this->content,
|
||||
Auth::id(),
|
||||
$this->directory ?: null
|
||||
$this->editingLocale,
|
||||
);
|
||||
|
||||
session()->flash('message', 'Document created successfully!');
|
||||
session()->flash('message', __('messages.documents.create_success'));
|
||||
return $this->redirect(route('documents.show', $this->document));
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
session()->flash('error', 'Error saving document: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteTranslation(DocumentService $documentService)
|
||||
{
|
||||
if (!$this->isEditMode || !$this->document || $this->isNewLocale) {
|
||||
return;
|
||||
}
|
||||
$this->authorize('update', $this->document);
|
||||
|
||||
try {
|
||||
$documentService->deleteTranslation($this->document, $this->editingLocale);
|
||||
session()->flash('message', __('messages.documents.translation_deleted'));
|
||||
return $this->redirect(route('documents.show', $this->document));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function setAsDefault(DocumentService $documentService)
|
||||
{
|
||||
if (!$this->isEditMode || !$this->document) {
|
||||
return;
|
||||
}
|
||||
$this->authorize('update', $this->document);
|
||||
|
||||
try {
|
||||
$this->document = $documentService->setDefaultLocale($this->document, $this->editingLocale);
|
||||
session()->flash('message', __('messages.documents.update_success'));
|
||||
return $this->redirect(route('documents.show', $this->document));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(DocumentService $documentService)
|
||||
{
|
||||
if (!$this->isEditMode || !$this->document) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->authorize('delete', $this->document);
|
||||
|
||||
try {
|
||||
$documentService->deleteDocument($this->document);
|
||||
session()->flash('message', 'Document deleted successfully!');
|
||||
|
||||
// Try to redirect to home document, or root if not found
|
||||
session()->flash('message', __('messages.documents.delete_success'));
|
||||
$homeDocument = Document::where('slug', 'home')->first();
|
||||
if ($homeDocument) {
|
||||
return redirect()->route('documents.show', $homeDocument);
|
||||
@@ -96,7 +153,9 @@ public function render()
|
||||
{
|
||||
return view('livewire.document-editor')
|
||||
->layout('layouts.knowledge-base', [
|
||||
'title' => $this->isEditMode ? 'Edit: ' . $this->title : 'New Document'
|
||||
'title' => $this->isEditMode
|
||||
? __('messages.documents.edit_document') . ': ' . $this->title
|
||||
: __('messages.documents.new_document'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,65 @@ class="inline-flex items-center justify-center px-3 sm:px-4 py-2 bg-indigo-600 b
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($isEditMode && $document)
|
||||
<div class="mb-4 border-b border-gray-200" role="tablist" aria-label="{{ __('messages.documents.translation_tabs_label') }}">
|
||||
<nav class="-mb-px flex flex-wrap gap-x-2">
|
||||
@php $allLocales = \App\Http\Middleware\SetLocale::SUPPORTED_LOCALES; @endphp
|
||||
@foreach($availableLocales as $loc)
|
||||
@php $isActive = ($loc === $editingLocale); @endphp
|
||||
<a href="{{ route('documents.translations.edit', ['document' => $document, 'locale' => $loc]) }}"
|
||||
class="px-3 py-2 text-sm font-medium border-b-2 {{ $isActive ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||
{{ __('messages.locale_names.' . $loc, [], 'en') }}
|
||||
@if($loc === $document->default_locale)
|
||||
<span class="ml-1 text-xs text-gray-400">★</span>
|
||||
@endif
|
||||
</a>
|
||||
@endforeach
|
||||
|
||||
@if($isNewLocale && $editingLocale)
|
||||
<span class="px-3 py-2 text-sm font-medium border-b-2 border-indigo-500 text-indigo-600">
|
||||
{{ __('messages.locale_names.' . $editingLocale, [], 'en') }}
|
||||
<span class="ml-1 text-xs text-gray-400">({{ __('messages.documents.new_document') }})</span>
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@php $missingLocales = array_diff(array_keys($allLocales), $availableLocales, $isNewLocale ? [$editingLocale] : []); @endphp
|
||||
@if(!empty($missingLocales))
|
||||
<div x-data="{ open: false }" class="relative">
|
||||
<button type="button" @click="open = !open"
|
||||
class="px-3 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
||||
+ {{ __('messages.documents.add_translation') }}
|
||||
</button>
|
||||
<div x-show="open" @click.outside="open = false" x-cloak
|
||||
class="absolute right-0 z-10 mt-2 w-48 max-h-64 overflow-y-auto bg-white border border-gray-200 rounded-md shadow-lg">
|
||||
@foreach($missingLocales as $loc)
|
||||
<a href="{{ route('documents.translations.edit', ['document' => $document, 'locale' => $loc]) }}"
|
||||
class="block px-3 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
||||
{{ $allLocales[$loc] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</nav>
|
||||
|
||||
@if($editingLocale !== $document->default_locale && !$isNewLocale)
|
||||
<div class="mt-2 flex gap-2">
|
||||
<button wire:click="setAsDefault" type="button"
|
||||
class="px-2 py-1 text-xs text-indigo-600 border border-indigo-300 rounded hover:bg-indigo-50">
|
||||
{{ __('messages.documents.set_as_default') }}
|
||||
</button>
|
||||
<button wire:click="deleteTranslation"
|
||||
wire:confirm="{{ __('messages.documents.delete_confirm') }}"
|
||||
type="button"
|
||||
class="px-2 py-1 text-xs text-red-600 border border-red-300 rounded hover:bg-red-50">
|
||||
{{ __('messages.documents.delete_translation') ?? __('messages.documents.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Form -->
|
||||
<form wire:submit.prevent="save" class="space-y-6">
|
||||
<!-- Title -->
|
||||
|
||||
@@ -65,4 +65,41 @@ public function test_no_banner_when_translation_exists(): void
|
||||
$response->assertOk();
|
||||
$response->assertDontSeeText(__('messages.documents.fallback_notice', [], 'ja'));
|
||||
}
|
||||
|
||||
public function test_editor_loads_existing_translation_for_locale(): void
|
||||
{
|
||||
$owner = \App\Models\User::factory()->create();
|
||||
$doc = Document::factory()->create(['default_locale' => 'en', 'created_by' => $owner->id, 'slug' => 'editor']);
|
||||
$doc->translations()->where('locale', 'en')->update(['title' => 'Hello', 'content' => 'EN body']);
|
||||
DocumentTranslation::factory()->create([
|
||||
'document_id' => $doc->id,
|
||||
'locale' => 'ja',
|
||||
'title' => 'こんにちは',
|
||||
'content' => 'JA body',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($owner)->get(route('documents.translations.edit', [
|
||||
'document' => $doc,
|
||||
'locale' => 'ja',
|
||||
]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('こんにちは');
|
||||
$response->assertSee('JA body');
|
||||
}
|
||||
|
||||
public function test_editor_for_missing_locale_shows_empty_form_with_new_locale_state(): void
|
||||
{
|
||||
$owner = \App\Models\User::factory()->create();
|
||||
$doc = Document::factory()->create(['default_locale' => 'en', 'created_by' => $owner->id, 'slug' => 'editor2']);
|
||||
|
||||
$response = $this->actingAs($owner)->get(route('documents.translations.edit', [
|
||||
'document' => $doc,
|
||||
'locale' => 'ja',
|
||||
]));
|
||||
|
||||
$response->assertOk();
|
||||
// The blade should render a tab marked active for ja with empty inputs
|
||||
$response->assertSeeText(__('messages.locale_names.ja', [], 'en'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user