diff --git a/app/Containers/AppStructure/Models/SubSection.php b/app/Containers/AppStructure/Models/SubSection.php index cf9aeae..00fb4f0 100755 --- a/app/Containers/AppStructure/Models/SubSection.php +++ b/app/Containers/AppStructure/Models/SubSection.php @@ -20,6 +20,6 @@ class SubSection extends Model public function pages() : HasMany { - return $this->hasMany(Page::class); + return $this->hasMany(Page::class)->orderBy('sort', 'asc'); } } diff --git a/app/Containers/AppStructure/UI/API/Transformers/PageNavigateResource.php b/app/Containers/AppStructure/UI/API/Transformers/PageNavigateResource.php index b7480a6..3f99862 100755 --- a/app/Containers/AppStructure/UI/API/Transformers/PageNavigateResource.php +++ b/app/Containers/AppStructure/UI/API/Transformers/PageNavigateResource.php @@ -19,8 +19,7 @@ class PageNavigateResource extends JsonResource 'title' => $this->title, 'slug' => $this->slug, 'path' => $this->path, - 'is_url' => $this->is_url, - 'icon' => $this->icon + 'is_url' => $this->is_url ]; } } diff --git a/app/Containers/AppStructure/UI/WEB/Transformers/PageResource.php b/app/Containers/AppStructure/UI/WEB/Transformers/PageResource.php index f244157..009c011 100755 --- a/app/Containers/AppStructure/UI/WEB/Transformers/PageResource.php +++ b/app/Containers/AppStructure/UI/WEB/Transformers/PageResource.php @@ -23,7 +23,6 @@ class PageResource extends JsonResource 'path' => $this->path, 'is_url' => $this->is_url, 'settings' => $this->settings, - 'icon' => $this->icon, 'section' => $this->section ? $this->section->title : null, 'created_at' => $this->created_at->diffforhumans() ]; diff --git a/app/Containers/Dashboard/Actions/Pages/CreatePageAction.php b/app/Containers/Dashboard/Actions/Pages/CreatePageAction.php index 9ad9019..786afa8 100644 --- a/app/Containers/Dashboard/Actions/Pages/CreatePageAction.php +++ b/app/Containers/Dashboard/Actions/Pages/CreatePageAction.php @@ -19,6 +19,9 @@ class CreatePageAction */ public function run(array $data): Page { + // Ensure icon is never saved even if sent from old forms + unset($data['icon']); + // Генерируем search_data из контента if (!empty($data['content'])) { $data['search_data'] = $this->generateSearchDataTask->run($data['content']); diff --git a/app/Containers/Dashboard/Actions/Pages/UpdatePageAction.php b/app/Containers/Dashboard/Actions/Pages/UpdatePageAction.php index 1f2ef20..99851ba 100644 --- a/app/Containers/Dashboard/Actions/Pages/UpdatePageAction.php +++ b/app/Containers/Dashboard/Actions/Pages/UpdatePageAction.php @@ -20,6 +20,9 @@ class UpdatePageAction */ public function run(Page $page, array $data): Page { + // Ensure icon is never saved even if sent from old forms + unset($data['icon']); + // Если контент изменился, перегенерируем search_data if (isset($data['content']) && json_encode($data['content']) !== json_encode($page->content)) { $data['search_data'] = $this->generateSearchDataTask->run($data['content']); diff --git a/app/Containers/Dashboard/Actions/Posts/DeploySiteAction.php b/app/Containers/Dashboard/Actions/Posts/DeploySiteAction.php index 9b96d4e..b562f92 100644 --- a/app/Containers/Dashboard/Actions/Posts/DeploySiteAction.php +++ b/app/Containers/Dashboard/Actions/Posts/DeploySiteAction.php @@ -37,9 +37,9 @@ class DeploySiteAction unlink($this->logFile); } - // Запускаем deploy.sh в фоне через sudo (полный путь + NOPASSWD для www-data) + // Запускаем deploy.sh в фоне (docker socket проброшен в контейнер) $command = sprintf( - '/usr/bin/sudo bash %s > %s 2>&1 &', + 'bash %s > %s 2>&1 &', escapeshellarg($this->deployScript), escapeshellarg($this->logFile) ); diff --git a/app/Containers/Dashboard/Tasks/Posts/CreatePostFromAiDataTask.php b/app/Containers/Dashboard/Tasks/Posts/CreatePostFromAiDataTask.php index 1c941f3..98c8861 100644 --- a/app/Containers/Dashboard/Tasks/Posts/CreatePostFromAiDataTask.php +++ b/app/Containers/Dashboard/Tasks/Posts/CreatePostFromAiDataTask.php @@ -49,7 +49,6 @@ class CreatePostFromAiDataTask [ 'type' => 'paragraph', 'data' => [ - 'seo_active' => true, 'content' => $newsData['body'] ?? '', ], ], diff --git a/app/Containers/Dashboard/Tests/Unit/VkPostGenerationTest.php b/app/Containers/Dashboard/Tests/Unit/VkPostGenerationTest.php index 8663059..55f83de 100644 --- a/app/Containers/Dashboard/Tests/Unit/VkPostGenerationTest.php +++ b/app/Containers/Dashboard/Tests/Unit/VkPostGenerationTest.php @@ -28,7 +28,6 @@ class VkPostGenerationTest extends TestCase [ 'type' => 'paragraph', 'data' => [ - 'seo_active' => true, 'content' => '
Это основной текст новости. Он содержит важную информацию о событии.
', ], ], @@ -115,7 +114,6 @@ class VkPostGenerationTest extends TestCase [ 'type' => 'paragraph', 'data' => [ - 'seo_active' => true, 'content' => 'Текст новости без файлов.
', ], ], diff --git a/app/Containers/Dashboard/UI/WEB/Controllers/SubSectionController.php b/app/Containers/Dashboard/UI/WEB/Controllers/SubSectionController.php index 836416f..9b48db8 100644 --- a/app/Containers/Dashboard/UI/WEB/Controllers/SubSectionController.php +++ b/app/Containers/Dashboard/UI/WEB/Controllers/SubSectionController.php @@ -6,6 +6,7 @@ use App\Containers\AppStructure\Models\Page; use App\Containers\AppStructure\Models\SubSection; use App\Containers\Dashboard\Actions\Pages\AttachPageToSubSectionAction; use App\Containers\Dashboard\Actions\Pages\DetachPageFromSubSectionAction; +use App\Containers\Dashboard\Actions\Pages\ReorderPagesAction; use App\Containers\Dashboard\Actions\SubSections\AttachSubSectionToMainSectionAction; use App\Containers\Dashboard\Actions\SubSections\CreateSubSectionAction; use App\Containers\Dashboard\Actions\SubSections\DeleteSubSectionAction; @@ -31,6 +32,7 @@ class SubSectionController extends Controller private readonly DetachSubSectionFromMainSectionAction $detachSubSectionAction, private readonly AttachPageToSubSectionAction $attachPageAction, private readonly DetachPageFromSubSectionAction $detachPageAction, + private readonly ReorderPagesAction $reorderPagesAction, ) {} /** @@ -82,7 +84,9 @@ class SubSectionController extends Controller */ public function edit(SubSection $subSection): \Inertia\Response { - $subSection->load(['mainSection', 'pages']); + $subSection->load(['mainSection', 'pages' => function ($query) { + $query->orderBy('sort', 'asc'); + }]); $mainSections = MainSection::pluck('title', 'id'); $availablePages = Page::whereNull('sub_section_id') @@ -200,4 +204,23 @@ class SubSectionController extends Controller return back()->with('error', 'Ошибка при откреплении страницы: ' . $e->getMessage()); } } + + /** + * Изменяет порядок страниц в подразделе + */ + public function reorderPages(Request $request, SubSection $subSection): RedirectResponse + { + $request->validate([ + 'page_ids' => ['required', 'array'], + 'page_ids.*' => ['integer', 'exists:pages,id'], + ]); + + try { + $this->reorderPagesAction->run($subSection->id, $request->page_ids); + + return back()->with('success', 'Порядок страниц обновлен!'); + } catch (\Exception $e) { + return back()->with('error', 'Ошибка при изменении порядка страниц: ' . $e->getMessage()); + } + } } diff --git a/app/Containers/Dashboard/UI/WEB/Requests/StorePageRequest.php b/app/Containers/Dashboard/UI/WEB/Requests/StorePageRequest.php index 503396a..2c14500 100644 --- a/app/Containers/Dashboard/UI/WEB/Requests/StorePageRequest.php +++ b/app/Containers/Dashboard/UI/WEB/Requests/StorePageRequest.php @@ -20,7 +20,6 @@ class StorePageRequest extends FormRequest 'sub_section_id' => ['nullable', 'exists:sub_sections,id'], 'code' => ['required', Rule::in(['200', '404', '500'])], 'searchable' => ['boolean'], - 'icon' => ['nullable', 'string'], 'content' => ['nullable', 'array'], 'settings' => ['nullable', 'array'], 'settings.hide_page_sub_section_links' => ['nullable', 'boolean'], diff --git a/app/Containers/Dashboard/UI/WEB/Requests/UpdatePageRequest.php b/app/Containers/Dashboard/UI/WEB/Requests/UpdatePageRequest.php index 32e080a..92a6cd4 100644 --- a/app/Containers/Dashboard/UI/WEB/Requests/UpdatePageRequest.php +++ b/app/Containers/Dashboard/UI/WEB/Requests/UpdatePageRequest.php @@ -20,7 +20,6 @@ class UpdatePageRequest extends FormRequest 'sub_section_id' => ['nullable', 'exists:sub_sections,id'], 'code' => ['required', Rule::in(['200', '404', '500'])], 'searchable' => ['boolean'], - 'icon' => ['nullable', 'string'], 'content' => ['nullable', 'array'], 'settings' => ['nullable', 'array'], 'settings.hide_page_sub_section_links' => ['nullable', 'boolean'], diff --git a/app/Containers/Dashboard/UI/WEB/Routes/web.php b/app/Containers/Dashboard/UI/WEB/Routes/web.php index f94387f..82c3bd9 100755 --- a/app/Containers/Dashboard/UI/WEB/Routes/web.php +++ b/app/Containers/Dashboard/UI/WEB/Routes/web.php @@ -356,6 +356,7 @@ Route::middleware(['access-check', 'dashboard.auth'])->group(function () { // Управление страницами (Relation Manager) Route::post('/{subSection}/pages/attach', [SubSectionController::class, 'attachPage'])->name('pages.attach'); Route::delete('/{subSection}/pages/{page}/detach', [SubSectionController::class, 'detachPage'])->name('pages.detach'); + Route::post('/{subSection}/pages/reorder', [SubSectionController::class, 'reorderPages'])->name('pages.reorder'); }); // CRUD страниц diff --git a/app/Filament/Components/Forms/ItemForm/Blocks/ParagraphBlock.php b/app/Filament/Components/Forms/ItemForm/Blocks/ParagraphBlock.php index a65108a..a4798f5 100755 --- a/app/Filament/Components/Forms/ItemForm/Blocks/ParagraphBlock.php +++ b/app/Filament/Components/Forms/ItemForm/Blocks/ParagraphBlock.php @@ -3,9 +3,6 @@ namespace App\Filament\Components\Forms\ItemForm\Blocks; use App\Filament\Components\Forms\ItemForm\Blocks\BlockSchema; -use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\Toggle; -use Filament\Forms\Get; use Mohamedsabil83\FilamentFormsTinyeditor\Components\TinyEditor; class ParagraphBlock implements BlockSchema @@ -14,16 +11,6 @@ class ParagraphBlock implements BlockSchema public static function schema(): array { return [ - Toggle::make('seo_active') - ->label('Использовать блок как SEO-текст') - ->helperText('Этот текст будет использоваться для SEO-оптимизации') - ->live(onBlur: true) - ->required() - ->disabled(function ($state, Get $get) { - $data = $get('../../'); - return self::findSeoActive($data) && !$state; - }) - ->dehydrated(), TinyEditor::make('content') ->label('Текст') ->placeholder('Начните вводить текст...') @@ -32,20 +19,4 @@ class ParagraphBlock implements BlockSchema ->helperText('Основное текстовое содержимое блока'), ]; } - - private static function findSeoActive(array $data) : bool - { - $bool = false; - - foreach ($data as $item) { - if ($item['type'] !== 'paragraph') { - continue; - } - if ($item['data']['seo_active'] === true) { - $bool = true; - break; - } - } - return $bool; - } -} \ No newline at end of file +} diff --git a/app/Filament/Components/Forms/ItemForm/Defaults/TabBuilderItem.php b/app/Filament/Components/Forms/ItemForm/Defaults/TabBuilderItem.php index 5200d1a..0eadfbd 100755 --- a/app/Filament/Components/Forms/ItemForm/Defaults/TabBuilderItem.php +++ b/app/Filament/Components/Forms/ItemForm/Defaults/TabBuilderItem.php @@ -53,16 +53,6 @@ class TabBuilderItem ->label('Текст') ->icon('heroicon-o-document-text') ->schema([ - Toggle::make('seo_active') - ->label('Использовать блок как SEO-текст') - ->helperText('Этот текст будет использоваться для SEO-оптимизации') - ->live(onBlur: true) - ->required() - ->disabled(function ($state, Forms\Get $get) { - $data = $get('../../'); - return self::findSeoActive($data) && !$state; - }) - ->dehydrated(), TinyEditor::make('content') ->label('Текст') ->placeholder('Начните вводить текст...') diff --git a/app/Filament/Components/Forms/PageForm.php b/app/Filament/Components/Forms/PageForm.php index 5f82a1e..9abf198 100755 --- a/app/Filament/Components/Forms/PageForm.php +++ b/app/Filament/Components/Forms/PageForm.php @@ -13,7 +13,6 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Form; use Illuminate\Support\Str; -use TomatoPHP\FilamentIcons\Components\IconPicker; class PageForm { @@ -109,11 +108,6 @@ class PageForm ->default(true) ->inline(false) ->helperText('Разрешить локальному поиску индексировать страницу'), - IconPicker::make('icon') - ->label('Иконка страницы') - ->default('heroicon-o-academic-cap') - ->helperText('Выберите иконку для отображения в навигации') - ->columns(6), TextInput::make('search_data') ->hidden(), ]), diff --git a/app/Services/Filament/Domain/Posts/PostDataProcessor.php b/app/Services/Filament/Domain/Posts/PostDataProcessor.php index 48d2b25..ece3b64 100755 --- a/app/Services/Filament/Domain/Posts/PostDataProcessor.php +++ b/app/Services/Filament/Domain/Posts/PostDataProcessor.php @@ -100,10 +100,7 @@ class PostDataProcessor return ''; } - $rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']); - if ($rowData === null) { - $rowData = $this->getFirstBlockByName('paragraph', $data['content']); - } + $rowData = $this->getFirstBlockByName('paragraph', $data['content']); $previewText = $rowData ? html_entity_decode(strip_tags($rowData['data']['content'])) : ''; return Str::limit($previewText, 160); @@ -221,21 +218,4 @@ class PostDataProcessor } return null; } - - /** - * Находит блок по SEO-активности. - * - * @param string $name - * @param array $content - * @return array|null - */ - private function getBlockBySeoActiveState(string $name, array $content): ?array - { - foreach ($content as $block) { - if ($block['type'] === $name && ($block['data']['seo_active'] ?? false)) { - return $block; - } - } - return null; - } } \ No newline at end of file diff --git a/app/Services/Filament/Domain/Posts/PostSeoGenerator.php b/app/Services/Filament/Domain/Posts/PostSeoGenerator.php index b1584f0..0a41d15 100755 --- a/app/Services/Filament/Domain/Posts/PostSeoGenerator.php +++ b/app/Services/Filament/Domain/Posts/PostSeoGenerator.php @@ -40,10 +40,7 @@ class PostSeoGenerator */ private function extractSeoDescription(array $content): string { - $rowData = $this->getBlockBySeoActiveState('paragraph', $content); - if ($rowData === null) { - $rowData = $this->getFirstBlockByName('paragraph', $content); - } + $rowData = $this->getFirstBlockByName('paragraph', $content); $description = $rowData ? html_entity_decode(strip_tags($rowData['data']['content'])) : ''; return Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160); @@ -77,29 +74,9 @@ class PostSeoGenerator return null; } - /** - * Находит блок по SEO-активности. - * - * @param string $name - * @param array $content - * @return array|null - */ - private function getBlockBySeoActiveState(string $name, array $content): ?array - { - foreach ($content as $block) { - if ($block['type'] === $name && ($block['data']['seo_active'] ?? false)) { - return $block; - } - } - return null; - } - public function setPreviewText(array $data): ?string { - $rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']); - if ($rowData === null) { - $rowData = $this->getFirstBlockByName('paragraph', $data['content']); - } + $rowData = $this->getFirstBlockByName('paragraph', $data['content']); if ($rowData !== null) { $previewText = html_entity_decode(strip_tags($rowData['data']['content'])); diff --git a/app/Services/Filament/Domain/Seo/SeoGeneratorService.php b/app/Services/Filament/Domain/Seo/SeoGeneratorService.php index df64657..3de6ca9 100755 --- a/app/Services/Filament/Domain/Seo/SeoGeneratorService.php +++ b/app/Services/Filament/Domain/Seo/SeoGeneratorService.php @@ -39,10 +39,7 @@ class SeoGeneratorService */ private function extractSeoDescription(array $content): string { - $rowData = $this->getBlockBySeoActiveState('paragraph', $content); - if ($rowData === null) { - $rowData = $this->getFirstBlockByName('paragraph', $content); - } + $rowData = $this->getFirstBlockByName('paragraph', $content); $description = $rowData ? html_entity_decode(strip_tags($rowData['data']['content'])) : ''; return Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160); @@ -75,21 +72,4 @@ class SeoGeneratorService } return null; } - - /** - * Находит блок по SEO-активности. - * - * @param string $name - * @param array $content - * @return array|null - */ - private function getBlockBySeoActiveState(string $name, array $content): ?array - { - foreach ($content as $block) { - if ($block['type'] === $name && ($block['data']['seo_active'] ?? false)) { - return $block; - } - } - return null; - } } \ No newline at end of file diff --git a/resources/js/Pages/Dashboard/Components/ContentBuilder/ContentBuilder.vue b/resources/js/Pages/Dashboard/Components/ContentBuilder/ContentBuilder.vue index 47f7a56..20890fc 100644 --- a/resources/js/Pages/Dashboard/Components/ContentBuilder/ContentBuilder.vue +++ b/resources/js/Pages/Dashboard/Components/ContentBuilder/ContentBuilder.vue @@ -291,7 +291,7 @@ export default { const blockDefaults = { heading: () => ({ id: `anchor-${Date.now()}`, content: '' }), - paragraph: () => ({ seo_active: true, content: '' }), + paragraph: () => ({ content: '' }), image: () => ({ url: '', alt: '' }), images: () => ({ url: [], alt: '' }), files: () => ({ file: [] }), diff --git a/resources/js/Pages/Dashboard/Components/ContentBuilder/TabContentBuilder.vue b/resources/js/Pages/Dashboard/Components/ContentBuilder/TabContentBuilder.vue index ffcf5f0..482469b 100644 --- a/resources/js/Pages/Dashboard/Components/ContentBuilder/TabContentBuilder.vue +++ b/resources/js/Pages/Dashboard/Components/ContentBuilder/TabContentBuilder.vue @@ -288,7 +288,7 @@ export default { const blockDefaults = { heading: () => ({ id: `anchor-${Date.now()}`, content: '' }), - paragraph: () => ({ seo_active: true, content: '' }), + paragraph: () => ({ content: '' }), image: () => ({ url: '', alt: '' }), images: () => ({ url: [], alt: '' }), files: () => ({ file: [] }), @@ -402,24 +402,26 @@ export default { onMounted(() => { if (props.modelValue && props.modelValue.length > 0) { blocks.value = props.modelValue.map(block => ({ - _uid: generateUid(), + _uid: block._uid || generateUid(), ...block })); } }); - // Watch for modelValue changes (e.g., when loading existing content) + // Watch for modelValue changes — only for initial load or when parent reloads + // DO NOT sync on every change to avoid infinite loops with emitChange watch( () => props.modelValue, (newValue) => { + // Only update if blocks are empty (page reload or tab switch) if (newValue && newValue.length > 0 && blocks.value.length === 0) { - // Only initialize if blocks are empty (initial load from server) blocks.value = newValue.map(block => ({ - _uid: generateUid(), + _uid: block._uid || generateUid(), ...block })); } - } + }, + { deep: true } ); return { diff --git a/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/ParagraphBlock.vue b/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/ParagraphBlock.vue index 72034d8..b09d9c7 100644 --- a/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/ParagraphBlock.vue +++ b/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/ParagraphBlock.vue @@ -1,17 +1,5 @@| + + | Заголовок страницы | @@ -178,10 +181,26 @@
|---|---|
|
+
+
+ |
{{ page.title }}
@@ -215,7 +234,7 @@
|
+