Skip to content

Commit 07052dd

Browse files
committed
Refactor config + add icons to tree items
1 parent 2cf1544 commit 07052dd

File tree

7 files changed

+177
-60
lines changed

7 files changed

+177
-60
lines changed

CLAUDE.md

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ php artisan flexible-content-block-pages:seed
4343
- **Settings** (`src/Models/Settings.php`): Global CMS settings and configuration
4444
- **Redirect** (`src/Models/Redirect.php`): URL redirect management
4545
- **Tag/TagType** (`src/Models/Tag.php`, `src/Models/TagType.php`): Content tagging system
46-
- **Menu/MenuItem** (`src/Models/Menu.php`, `src/Models/MenuItem.php`): Hierarchical menu system with nested structure using kalnoy/nestedset
46+
- **Menu/MenuItem** (`src/Models/Menu.php`, `src/Models/MenuItem.php`): Hierarchical menu system using solution-forest/filament-tree with parent_id/order structure
4747

4848
### Key Components
4949
- **Filament Resources**: Located in `src/Resources/` - provide admin interface for all models
@@ -78,42 +78,58 @@ Pages use the flexible content blocks system from the parent package, allowing:
7878
- Spatie packages for tags, media library, and redirects
7979
- Laravel Localization for multilingual support
8080
- SEOTools for meta tag and OpenGraph management
81-
- kalnoy/nestedset for hierarchical menu structure
81+
- solution-forest/filament-tree for hierarchical menu structure
8282

83-
## Menu Builder System (In Development)
83+
## Menu Builder System
8484

8585
### Current Status
86-
The menu builder is partially implemented with the following components:
86+
The menu builder is **fully implemented** using solution-forest/filament-tree with the following components:
8787

8888
**✅ Completed:**
89-
- Menu and MenuItem models with kalnoy/nestedset integration
90-
- Database migrations for menus and menu_items tables
91-
- MenuResource with basic CRUD operations
92-
- ManageMenuItems page with drag-and-drop tree interface
93-
- Alpine.js + SortableJS powered tree builder component
94-
- Translation support (EN/NL)
95-
- Configuration system with max depth settings
96-
97-
**🚧 Remaining Tasks:**
98-
- MenuItem form/modal for adding and editing menu items
99-
- Linkable model integration (following call-to-action patterns)
100-
- Frontend helper methods and blade components for menu rendering
101-
- Validation and error handling
102-
- Proper nested set operations for reordering
89+
- Menu and MenuItem models with solution-forest/filament-tree integration
90+
- Database migrations with parent_id/order structure (migrated from kalnoy/nestedset)
91+
- MenuResource with CRUD operations and enhanced management
92+
- ManageMenuItems page with drag-and-drop tree interface using solution-forest/filament-tree
93+
- Complete MenuItem form with dynamic type selection (URL, Route, Linkable Model)
94+
- Linkable model integration with polymorphic relationships
95+
- Enhanced tree display with icons and translated model labels
96+
- Translation support for all menu components
97+
- Frontend menu rendering components for various styles
98+
99+
**🔧 Key Features:**
100+
- **Enhanced Tree Interface**: Icons indicate item type and visibility (eye-slash for hidden items)
101+
- **Resource Integration**: Uses Filament resource labels and icons for linkable models
102+
- **Smart Descriptions**: Shows route URLs instead of names, translated model labels
103+
- **Flexible Configuration**: Structured linkable_models config with class/resource mapping
104+
- **Translation Support**: Full multilingual support with translatable labels
103105

104106
### Architecture
105-
- **Menu Model**: Simple container with name, code, and description
106-
- **MenuItem Model**: Nested set structure with linkable polymorphic relationships
107-
- **ManageMenuItems Page**: Dedicated interface for menu structure management
108-
- **Tree Builder Component**: Custom Filament field with Alpine.js interactions
107+
- **Menu Model**: Container with name, code, description, and configurable styles
108+
- **MenuItem Model**: Simple tree structure (parent_id/order) with ModelTree trait
109+
- **ManageMenuItems Page**: Full tree management with create/edit/delete actions
110+
- **MenuItemForm**: Dynamic form supporting URL, Route, and Model linking types
111+
- **Frontend Components**: Menu rendering with multiple style support
109112

110113
### Configuration
111114
```php
112115
'menu' => [
113-
'max_depth' => 2, // Configurable nesting level
116+
'max_depth' => 2,
117+
'linkable_models' => [
118+
[
119+
'class' => \Statikbe\FilamentFlexibleContentBlockPages\Models\Page::class,
120+
'resource' => \Statikbe\FilamentFlexibleContentBlockPages\Resources\PageResource::class,
121+
],
122+
],
123+
'styles' => ['default', 'horizontal', 'vertical', 'dropdown'],
114124
],
115125
```
116126

127+
**Migration Notes:**
128+
- Successfully migrated from 15web/filament-tree + kalnoy/nestedset to solution-forest/filament-tree
129+
- Removed complex nested set structure (_lft, _rgt) in favor of simple parent_id/order
130+
- Enhanced with resource-based translations and icons
131+
- Backward compatibility not maintained (no existing projects using menus)
132+
117133
## Development Guidelines
118134

119135
### Use Existing Filament Components

config/filament-flexible-content-block-pages.php

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,25 +100,24 @@
100100
],
101101

102102
'menu' => [
103-
104103
'max_depth' => 2,
105104
'linkable_models' => [
106105
// Models that can be linked in menu items
107106
// These models must implement HasMenuLabel interface
108-
\Statikbe\FilamentFlexibleContentBlockPages\Models\Page::class,
107+
[
108+
'class' => \Statikbe\FilamentFlexibleContentBlockPages\Models\Page::class,
109+
'resource' => \Statikbe\FilamentFlexibleContentBlockPages\Resources\PageResource::class,
110+
],
109111

110112
// Add your own models here:
111-
// \App\Models\Category::class,
112-
// \App\Models\Product::class,
113-
],
114-
'model_icons' => [
115-
// Configure icons for different model types based on their morph class
116-
'filament-flexible-content-block-pages::page' => 'heroicon-o-document-text',
117-
118-
// Add custom icons for your models:
119-
// 'category' => 'heroicon-o-tag',
120-
// 'product' => 'heroicon-o-shopping-bag',
121-
// 'post' => 'heroicon-o-newspaper',
113+
// [
114+
// 'class' => \App\Models\Category::class,
115+
// 'resource' => \App\Filament\Resources\CategoryResource::class,
116+
// ],
117+
// [
118+
// 'class' => \App\Models\Product::class,
119+
// 'resource' => \App\Filament\Resources\ProductResource::class,
120+
// ],
122121
],
123122
'styles' => [
124123
// Available menu styles (codes only - labels come from translations)

src/FilamentFlexibleContentBlockPagesConfig.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,31 @@ public function getMenuLinkableModels(): array
160160
return $this->packageConfig('menu.linkable_models', []);
161161
}
162162

163-
public function getMenuModelIcons(): array
163+
public function getMenuLinkableModelClasses(): array
164164
{
165-
return $this->packageConfig('menu.model_icons', []);
165+
$models = $this->getMenuLinkableModels();
166+
$classes = [];
167+
168+
foreach ($models as $model) {
169+
if (isset($model['class'])) {
170+
$classes[] = $model['class'];
171+
}
172+
}
173+
174+
return $classes;
175+
}
176+
177+
public function getMenuLinkableModelResource(string $modelClass): ?string
178+
{
179+
$models = $this->getMenuLinkableModels();
180+
181+
foreach ($models as $model) {
182+
if (isset($model['class'], $model['resource']) && $model['class'] === $modelClass) {
183+
return $model['resource'];
184+
}
185+
}
186+
187+
return null;
166188
}
167189

168190
public function getMenuStyles(): array

src/Form/Fields/Types/LinkableMenuItemType.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66
use Filament\Forms\Components\Select;
77
use Statikbe\FilamentFlexibleContentBlockPages\Facades\FilamentFlexibleContentBlockPages;
88
use Statikbe\FilamentFlexibleContentBlockPages\Models\Contracts\HasMenuLabel;
9-
use function Statikbe\FilamentFlexibleContentBlockPages\Filament\Form\Fields\Types\call_user_func;
10-
use function Statikbe\FilamentFlexibleContentBlockPages\Filament\Form\Fields\Types\is_subclass_of;
11-
use function Statikbe\FilamentFlexibleContentBlockPages\Filament\Form\Fields\Types\method_exists;
129

1310
class LinkableMenuItemType extends AbstractMenuItemType
1411
{

src/Form/Fields/Types/RouteMenuItemType.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
use Filament\Forms\Components\Select;
66
use Illuminate\Support\Facades\Route;
7-
use function Statikbe\FilamentFlexibleContentBlockPages\Filament\Form\Fields\Types\fnmatch;
8-
use function Statikbe\FilamentFlexibleContentBlockPages\Filament\Form\Fields\Types\ksort;
97

108
class RouteMenuItemType extends AbstractMenuItemType
119
{

src/Form/Forms/MenuItemForm.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88
use Filament\Forms\Components\TextInput;
99
use Filament\Forms\Components\Toggle;
1010
use Filament\Forms\Get;
11+
use Statikbe\FilamentFlexibleContentBlockPages\Facades\FilamentFlexibleContentBlockPages;
1112
use Statikbe\FilamentFlexibleContentBlockPages\Form\Fields\LabelField;
1213
use Statikbe\FilamentFlexibleContentBlockPages\Form\Fields\Types\AbstractMenuItemType;
1314
use Statikbe\FilamentFlexibleContentBlockPages\Form\Fields\Types\LinkableMenuItemType;
1415
use Statikbe\FilamentFlexibleContentBlockPages\Form\Fields\Types\RouteMenuItemType;
1516
use Statikbe\FilamentFlexibleContentBlockPages\Form\Fields\Types\UrlMenuItemType;
1617
use Statikbe\FilamentFlexibleContentBlockPages\Models\Contracts\HasMenuLabel;
1718
use Statikbe\FilamentFlexibleContentBlocks\FilamentFlexibleBlocksConfig;
18-
use function Statikbe\FilamentFlexibleContentBlockPages\Filament\Form\Forms\is_string;
19-
use function Statikbe\FilamentFlexibleContentBlockPages\Filament\Form\Forms\is_subclass_of;
2019

2120
class MenuItemForm
2221
{
@@ -154,8 +153,10 @@ protected static function getLinkableField(): Select
154153
$type = static::getTypeByAlias($linkType);
155154

156155
if ($type && $type->isModelType()) {
156+
$modelLabel = static::getModelLabelFromResource($type->getModel());
157+
157158
return flexiblePagesTrans('menu_items.form.linkable_help', [
158-
'model' => class_basename($type->getModel()),
159+
'model' => $modelLabel,
159160
]);
160161
}
161162

@@ -246,11 +247,29 @@ protected static function getTypeLabel(AbstractMenuItemType $type): string
246247
return flexiblePagesTrans('menu_items.form.types.route');
247248
}
248249

250+
// Get translated model label from Filament resource
251+
$modelLabel = static::getModelLabelFromResource($type->getModel());
252+
249253
return flexiblePagesTrans('menu_items.form.types.model', [
250-
'model' => class_basename($type->getModel()),
254+
'model' => $modelLabel,
251255
]);
252256
}
253257

258+
protected static function getModelLabelFromResource(string $modelClass): string
259+
{
260+
$resourceClass = \Statikbe\FilamentFlexibleContentBlockPages\Facades\FilamentFlexibleContentBlockPages::config()->getMenuLinkableModelResource($modelClass);
261+
262+
if ($resourceClass && class_exists($resourceClass)) {
263+
try {
264+
return $resourceClass::getModelLabel();
265+
} catch (\Exception $e) {
266+
// Fallback to class basename if resource method fails
267+
}
268+
}
269+
270+
return class_basename($modelClass);
271+
}
272+
254273
protected static function getTypes(): array
255274
{
256275
if (static::$types === null) {
@@ -260,10 +279,10 @@ protected static function getTypes(): array
260279
];
261280

262281
// Add configured linkable models from config
263-
$configuredModels = config('filament-flexible-content-block-pages.menu.linkable_models', []);
282+
$configuredModels = FilamentFlexibleContentBlockPages::config()->getMenuLinkableModelClasses();
264283

265284
foreach ($configuredModels as $modelClass) {
266-
if (is_string($modelClass) && is_subclass_of($modelClass, HasMenuLabel::class)) {
285+
if (is_subclass_of($modelClass, HasMenuLabel::class)) {
267286
static::$types[] = new LinkableMenuItemType($modelClass);
268287
}
269288
}

src/Resources/MenuResource/Pages/ManageMenuItems.php

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ class ManageMenuItems extends TreePage
2323

2424
public mixed $menu;
2525

26-
protected static int $maxDepth = 3;
27-
2826
public function mount(): void
2927
{
3028
$menuModelClass = MenuResource::getModel();
@@ -78,7 +76,7 @@ protected function getTreeActions(): array
7876
return [
7977
EditAction::make()
8078
->mountUsing(
81-
function($arguments, $form, $model, MenuItem $record) {
79+
function ($arguments, $form, $model, MenuItem $record) {
8280
$data = [
8381
...$record->toArray(),
8482
'menu_id' => $this->record->id,
@@ -127,28 +125,96 @@ public function getTreeRecordDescription(?Model $record = null): string|HtmlStri
127125
protected function getMenuItemTypeDescription(MenuItem $record): string
128126
{
129127
if ($record->linkable_type && $record->linkable) {
130-
return flexiblePagesTrans('menu_items.tree.linked_to').' '.class_basename($record->linkable_type);
131-
}
128+
// Get model label from Filament resource if available
129+
$modelLabel = $this->getModelLabelFromResource($record->linkable_type);
132130

133-
if ($record->url) {
131+
return flexiblePagesTrans('menu_items.tree.linked_to').' '.$modelLabel;
132+
} elseif ($record->url) {
134133
return flexiblePagesTrans('menu_items.tree.external_url').': '.$record->url;
134+
} elseif ($record->route) {
135+
// Show route URL instead of route name
136+
$routeUrl = $this->getRouteUrl($record->route);
137+
138+
return flexiblePagesTrans('menu_items.tree.route').': '.($routeUrl ?: $record->route);
139+
} else {
140+
return flexiblePagesTrans('menu_items.tree.no_link');
135141
}
142+
}
136143

137-
if ($record->route) {
138-
return flexiblePagesTrans('menu_items.tree.route').': '.$record->route;
144+
protected function getModelLabelFromResource(string $modelClass): string
145+
{
146+
$resourceClass = FilamentFlexibleContentBlockPages::config()->getMenuLinkableModelResource($modelClass);
147+
148+
if ($resourceClass && class_exists($resourceClass)) {
149+
try {
150+
return $resourceClass::getModelLabel();
151+
} catch (\Exception $e) {
152+
// Fallback to class basename if resource method fails
153+
}
154+
}
155+
156+
return class_basename($modelClass);
157+
}
158+
159+
protected function getModelIconFromResource(string $modelClass): ?string
160+
{
161+
$resourceClass = FilamentFlexibleContentBlockPages::config()->getMenuLinkableModelResource($modelClass);
162+
163+
if ($resourceClass && class_exists($resourceClass)) {
164+
try {
165+
return $resourceClass::getNavigationIcon();
166+
} catch (\Exception $e) {
167+
// Fallback to null if resource method fails
168+
}
139169
}
140170

141-
return flexiblePagesTrans('menu_items.tree.no_link');
171+
return null;
172+
}
173+
174+
protected function getRouteUrl(string $routeName): ?string
175+
{
176+
try {
177+
return route($routeName);
178+
} catch (\Exception $e) {
179+
return null;
180+
}
142181
}
143182

144183
public function getTreeRecordIcon(?\Illuminate\Database\Eloquent\Model $record = null): ?string
145184
{
146-
// TODO
147-
return parent::getTreeRecordIcon($record);
185+
/** @var MenuItem $record */
186+
if (! $record) {
187+
return null;
188+
}
189+
190+
// If not visible, show eye-slash icon
191+
if (! $record->is_visible) {
192+
return 'heroicon-o-eye-slash';
193+
}
194+
195+
// Return appropriate icon based on type
196+
if ($record->linkable_type && $record->linkable) {
197+
return $this->getModelIconFromResource($record->linkable_type) ?: 'heroicon-o-link';
198+
}
199+
200+
if ($record->url) {
201+
return 'heroicon-o-globe-alt';
202+
}
203+
204+
if ($record->route) {
205+
return 'heroicon-o-command-line';
206+
}
207+
208+
return 'heroicon-o-bars-3';
148209
}
149210

150211
protected function getFormSchema(): array
151212
{
152213
return MenuItemForm::getSchema();
153214
}
215+
216+
public static function getMaxDepth(): int
217+
{
218+
return FilamentFlexibleContentBlockPages::config()->getMenuMaxDepth() ?? 3;
219+
}
154220
}

0 commit comments

Comments
 (0)