Add Menu Tree functionality and integrate Filament Tree plugin: Introduced a new MenuTree page for managing page structures, added PagesMenuWidget for displaying a tree layout of pages, and updated localization files for menu-related actions. Enhanced Page model with tree management capabilities and included necessary CSS and JS files for the Filament Tree plugin.

This commit is contained in:
Ümit Tunç
2025-11-01 16:13:59 -03:00
parent 1d46c9243d
commit ad20ba575a
13 changed files with 342 additions and 2 deletions
@@ -5,6 +5,7 @@ namespace App\Filament\Admin\Resources\Pages;
use App\Filament\Admin\Resources\Pages\Pages\CreatePage; use App\Filament\Admin\Resources\Pages\Pages\CreatePage;
use App\Filament\Admin\Resources\Pages\Pages\EditPage; use App\Filament\Admin\Resources\Pages\Pages\EditPage;
use App\Filament\Admin\Resources\Pages\Pages\ListPages; use App\Filament\Admin\Resources\Pages\Pages\ListPages;
use App\Filament\Admin\Resources\Pages\Pages\MenuTree;
use App\Filament\Admin\Resources\Pages\Schemas\PageForm; use App\Filament\Admin\Resources\Pages\Schemas\PageForm;
use App\Filament\Admin\Resources\Pages\Tables\PagesTable; use App\Filament\Admin\Resources\Pages\Tables\PagesTable;
use App\Models\Page; use App\Models\Page;
@@ -60,6 +61,7 @@ class PageResource extends Resource
'index' => ListPages::route('/'), 'index' => ListPages::route('/'),
'create' => CreatePage::route('/create'), 'create' => CreatePage::route('/create'),
'edit' => EditPage::route('/{record}/edit'), 'edit' => EditPage::route('/{record}/edit'),
'menu' => MenuTree::route('/menu'),
]; ];
} }
@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Pages\Pages; namespace App\Filament\Admin\Resources\Pages\Pages;
use App\Filament\Admin\Resources\Pages\PageResource; use App\Filament\Admin\Resources\Pages\PageResource;
use Filament\Actions\Action;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
@@ -18,6 +19,11 @@ class ListPages extends ListRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Action::make('menu')
->label(__('pages.menu_tree_title'))
->icon('heroicon-o-bars-3')
->url(PageResource::getUrl('menu'))
->color('gray'),
CreateAction::make() CreateAction::make()
->label(__('pages.create')), ->label(__('pages.create')),
]; ];
@@ -0,0 +1,46 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Pages;
use App\Filament\Admin\Resources\Pages\PageResource;
use App\Filament\Admin\Widgets\PagesMenuWidget;
use Filament\Actions\Action;
use Filament\Resources\Pages\Page;
class MenuTree extends Page
{
protected static string $resource = PageResource::class;
protected string $view = 'filament.admin.pages.menu-tree';
public function getTitle(): string
{
return __('pages.menu_tree_title');
}
protected function getHeaderActions(): array
{
return [
Action::make('refresh')
->label(__('pages.menu_refresh'))
->icon('heroicon-o-arrow-path')
->color('gray')
->action(function () {
$this->dispatch('$refresh');
}),
Action::make('back_to_pages')
->label(__('pages.menu_back_to_pages'))
->icon('heroicon-o-arrow-left')
->url(PageResource::getUrl('index'))
->color('gray'),
];
}
protected function getHeaderWidgets(): array
{
return [
PagesMenuWidget::class,
];
}
}
@@ -0,0 +1,115 @@
<?php
namespace App\Filament\Admin\Widgets;
use App\Filament\Admin\Resources\Pages\PageResource;
use App\Models\Page;
use Illuminate\Database\Eloquent\Builder;
use SolutionForest\FilamentTree\Actions\EditAction;
use SolutionForest\FilamentTree\Actions\ViewAction;
use SolutionForest\FilamentTree\Widgets\Tree as BaseWidget;
class PagesMenuWidget extends BaseWidget
{
protected static string $model = Page::class;
protected static int $maxDepth = 4;
protected ?string $treeTitle = null;
protected bool $enableTreeTitle = true;
public function mount(): void
{
parent::mount();
$this->treeTitle = __('pages.menu_tree_title');
}
protected function getTreeQuery(): Builder
{
return Page::query()
->where('status', 'published')
->where('show_in_menu', true)
->orderBy('sort_order');
}
public function getTreeRecordTitle(?\Illuminate\Database\Eloquent\Model $record = null): string
{
if (!$record) {
return '';
}
$title = $record->title;
// Add child count indicator
$childCount = $record->children()
->where('status', 'published')
->where('show_in_menu', true)
->count();
if ($childCount > 0) {
$title .= " ({$childCount})";
}
return $title;
}
public function getTreeRecordIcon(?\Illuminate\Database\Eloquent\Model $record = null): ?string
{
if (!$record) {
return null;
}
// Root pages get folder icon (parent_id is null)
if ($record->parent_id === null) {
return 'heroicon-o-folder';
}
// Homepage gets special icon
if ($record->is_homepage) {
return 'heroicon-o-home';
}
// Child pages get document icon
return 'heroicon-o-document-text';
}
protected function hasDeleteAction(): bool
{
return false;
}
protected function hasEditAction(): bool
{
return true;
}
protected function hasViewAction(): bool
{
return true;
}
protected function configureEditAction(EditAction $action): EditAction
{
return $action
->icon('heroicon-o-pencil')
->label(__('pages.edit'))
->url(fn (Page $record): string => PageResource::getUrl('edit', ['record' => $record]))
->openUrlInNewTab(false);
}
protected function configureViewAction(ViewAction $action): ViewAction
{
return $action
->icon('heroicon-o-eye')
->label(__('pages.view'))
->url(fn (Page $record): string => $record->url)
->openUrlInNewTab(true);
}
public function getNodeCollapsedState(?\Illuminate\Database\Eloquent\Model $record = null): bool
{
return true;
}
}
+25 -1
View File
@@ -7,10 +7,11 @@ use App\Traits\HasTranslations;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use SolutionForest\FilamentTree\Concern\ModelTree;
class Page extends Model class Page extends Model
{ {
use HasFactory, SoftDeletes, HasTranslations; use HasFactory, SoftDeletes, HasTranslations, ModelTree;
protected $fillable = [ protected $fillable = [
'title', 'title',
@@ -172,4 +173,27 @@ class Page extends Model
]; ];
})->filter(fn($section) => $section['template'] !== null); })->filter(fn($section) => $section['template'] !== null);
} }
/**
* ModelTree - Custom column names
*/
public function determineOrderColumnName(): string
{
return 'sort_order';
}
public function determineParentColumnName(): string
{
return 'parent_id';
}
public function determineTitleColumnName(): string
{
return 'title';
}
public static function defaultParentKey()
{
return null; // Mevcut migration nullable olduğu için null kullanıyoruz
}
} }
+1
View File
@@ -11,6 +11,7 @@
"filament/filament": "~4.0", "filament/filament": "~4.0",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",
"solution-forest/filament-tree": "^3.0",
"spatie/laravel-permission": "^6.21", "spatie/laravel-permission": "^6.21",
"tomatophp/filament-users": "^4.0" "tomatophp/filament-users": "^4.0"
}, },
Generated
+72 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ae5eda2a09c03c50635001cd6005dbec", "content-hash": "131798d367c222bf82f94782fbafa942",
"packages": [ "packages": [
{ {
"name": "anourvalar/eloquent-serialize", "name": "anourvalar/eloquent-serialize",
@@ -5395,6 +5395,77 @@
], ],
"time": "2022-12-17T21:53:22+00:00" "time": "2022-12-17T21:53:22+00:00"
}, },
{
"name": "solution-forest/filament-tree",
"version": "3.1.3",
"source": {
"type": "git",
"url": "https://github.com/solutionforest/filament-tree.git",
"reference": "2722a10d782585c6063ec50903eb53f186e5202f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/solutionforest/filament-tree/zipball/2722a10d782585c6063ec50903eb53f186e5202f",
"reference": "2722a10d782585c6063ec50903eb53f186e5202f",
"shasum": ""
},
"require": {
"filament/filament": "^4.0",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.15.0"
},
"require-dev": {
"larastan/larastan": "^3.0",
"laravel/pint": "^1.0",
"nunomaduro/collision": "^8.0",
"orchestra/testbench": "^9.0|^10.0",
"pestphp/pest": "^3.0",
"pestphp/pest-plugin-arch": "^3.0",
"pestphp/pest-plugin-laravel": "^3.0",
"pestphp/pest-plugin-livewire": "^3.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"rector/rector": "^2.0",
"spatie/laravel-ray": "^1.26"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"SolutionForest\\FilamentTree\\FilamentTreeServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"SolutionForest\\FilamentTree\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Carly",
"email": "info@solutionforest.net",
"role": "Developer"
}
],
"description": "This is a tree layout plugin for Filament Admin",
"homepage": "https://github.com/solution-forest/filament-tree",
"keywords": [
"Solution Forest",
"filament-tree",
"laravel"
],
"support": {
"issues": "https://github.com/solution-forest/filament-tree/issues",
"source": "https://github.com/solution-forest/filament-tree"
},
"time": "2025-10-22T03:25:18+00:00"
},
{ {
"name": "spatie/invade", "name": "spatie/invade",
"version": "2.1.0", "version": "2.1.0",
+7
View File
@@ -16,12 +16,19 @@ return [
// Actions // Actions
'create' => 'Create New Page', 'create' => 'Create New Page',
'edit' => 'Edit Page', 'edit' => 'Edit Page',
'view' => 'View',
'save' => 'Save', 'save' => 'Save',
'cancel' => 'Cancel', 'cancel' => 'Cancel',
'delete' => 'Delete Page', 'delete' => 'Delete Page',
'restore' => 'Restore Page', 'restore' => 'Restore Page',
'force_delete' => 'Force Delete', 'force_delete' => 'Force Delete',
// Menu Tree
'menu_tree_title' => 'Menu Structure',
'menu_refresh' => 'Refresh',
'menu_back_to_pages' => 'Back to Pages',
'menu_tree_saved' => 'Menu structure saved successfully.',
// Form sections // Form sections
'form_section_content' => 'Content', 'form_section_content' => 'Content',
'form_section_page_settings' => 'Page Settings', 'form_section_page_settings' => 'Page Settings',
+7
View File
@@ -16,12 +16,19 @@ return [
// Actions // Actions
'create' => 'Yeni Sayfa Oluştur', 'create' => 'Yeni Sayfa Oluştur',
'edit' => 'Sayfayı Düzenle', 'edit' => 'Sayfayı Düzenle',
'view' => 'Görüntüle',
'save' => 'Kaydet', 'save' => 'Kaydet',
'cancel' => 'İptal', 'cancel' => 'İptal',
'delete' => 'Sayfayı Sil', 'delete' => 'Sayfayı Sil',
'restore' => 'Sayfayı Geri Yükle', 'restore' => 'Sayfayı Geri Yükle',
'force_delete' => 'Kalıcı Olarak Sil', 'force_delete' => 'Kalıcı Olarak Sil',
// Menu Tree
'menu_tree_title' => 'Menü Yapısı',
'menu_refresh' => 'Yenile',
'menu_back_to_pages' => 'Sayfalara Dön',
'menu_tree_saved' => 'Menü yapısı başarıyla kaydedildi.',
// Form sections // Form sections
'form_section_content' => 'İçerik', 'form_section_content' => 'İçerik',
'form_section_page_settings' => 'Sayfa Ayarları', 'form_section_page_settings' => 'Sayfa Ayarları',
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
<x-filament-panels::page>
@if ($this->hasHeaderWidgets())
<x-filament-widgets::widgets
:widgets="$this->getHeaderWidgets()"
:columns="$this->getHeaderWidgetsColumns()"
/>
@endif
</x-filament-panels::page>