changes
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
];
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -49,7 +49,6 @@ class CreatePostFromAiDataTask
|
||||
[
|
||||
'type' => 'paragraph',
|
||||
'data' => [
|
||||
'seo_active' => true,
|
||||
'content' => $newsData['body'] ?? '',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -28,7 +28,6 @@ class VkPostGenerationTest extends TestCase
|
||||
[
|
||||
'type' => 'paragraph',
|
||||
'data' => [
|
||||
'seo_active' => true,
|
||||
'content' => '<p>Это основной текст новости. Он содержит важную информацию о событии.</p>',
|
||||
],
|
||||
],
|
||||
@@ -115,7 +114,6 @@ class VkPostGenerationTest extends TestCase
|
||||
[
|
||||
'type' => 'paragraph',
|
||||
'data' => [
|
||||
'seo_active' => true,
|
||||
'content' => '<p>Текст новости без файлов.</p>',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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 страниц
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('Начните вводить текст...')
|
||||
|
||||
@@ -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(),
|
||||
]),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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']));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user