Add Menu Template functionality: Created MenuTemplate resource with CRUD pages, schemas, and table configuration. Implemented localization for menu template fields and integrated menu rendering capabilities. Enhanced TemplatePreviewController to support menu templates and updated dynamic template documentation accordingly.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates;
|
||||
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Pages\CreateMenuTemplate;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Pages\EditMenuTemplate;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Pages\ListMenuTemplates;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Pages\ViewMenuTemplate;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Schemas\MenuTemplateForm;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Schemas\MenuTemplateInfolist;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Tables\MenuTemplatesTable;
|
||||
use App\Models\MenuTemplate;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class MenuTemplateResource extends Resource
|
||||
{
|
||||
protected static ?string $model = MenuTemplate::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return __('menu-templates.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('menu-templates.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('menu-templates.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('menu-templates.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return MenuTemplateForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return MenuTemplateInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return MenuTemplatesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListMenuTemplates::route('/'),
|
||||
'create' => CreateMenuTemplate::route('/create'),
|
||||
'view' => ViewMenuTemplate::route('/{record}'),
|
||||
'edit' => EditMenuTemplate::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRecordRouteBindingEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getRecordRouteBindingEloquentQuery()
|
||||
->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MenuTemplates\MenuTemplateResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class CreateMenuTemplate extends CreateRecord
|
||||
{
|
||||
protected static string $resource = MenuTemplateResource::class;
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MenuTemplates\MenuTemplateResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class EditMenuTemplate extends EditRecord
|
||||
{
|
||||
protected static string $resource = MenuTemplateResource::class;
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MenuTemplates\MenuTemplateResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListMenuTemplates extends ListRecords
|
||||
{
|
||||
protected static string $resource = MenuTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MenuTemplates\MenuTemplateResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewMenuTemplate extends ViewRecord
|
||||
{
|
||||
protected static string $resource = MenuTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Schemas;
|
||||
|
||||
use App\Models\MenuTemplate;
|
||||
use Filament\Forms\Components\CodeEditor;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\View;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\CodeEditor\Enums\Language;
|
||||
|
||||
class MenuTemplateForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make(__('menu-templates.section_general'))
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label(__('menu-templates.field_title'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpanFull(),
|
||||
|
||||
CodeEditor::make('html_content')
|
||||
->label(__('menu-templates.field_html_content'))
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->language(Language::Html)
|
||||
->columnSpanFull()
|
||||
->helperText(__('menu-templates.field_html_content_help'))
|
||||
->extraAttributes([
|
||||
'style' => 'max-height: 400px;overflow-y: auto;',
|
||||
'data-field-name' => 'html_content',
|
||||
]),
|
||||
|
||||
// Preview Component
|
||||
View::make('components.menu-template-preview')
|
||||
->viewData(function ($record) {
|
||||
return [
|
||||
'type' => 'menu',
|
||||
'fieldName' => 'html_content',
|
||||
'recordId' => $record?->id,
|
||||
];
|
||||
})
|
||||
->columnSpanFull(),
|
||||
|
||||
Toggle::make('is_active')
|
||||
->label(__('menu-templates.field_is_active'))
|
||||
->default(true)
|
||||
->inline(false),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Schemas;
|
||||
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class MenuTemplateInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
//
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class MenuTemplatesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('title')
|
||||
->label(__('menu-templates.column_title'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
IconColumn::make('is_active')
|
||||
->label(__('menu-templates.column_is_active'))
|
||||
->boolean()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('menu-templates.column_created_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('updated_at')
|
||||
->label(__('menu-templates.column_updated_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('deleted_at')
|
||||
->label(__('menu-templates.column_deleted_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
TrashedFilter::make(),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
ForceDeleteBulkAction::make(),
|
||||
RestoreBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
||||
use App\Models\HeaderTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Models\SectionTemplate;
|
||||
use App\Models\MenuTemplate;
|
||||
use App\Services\TemplatePreviewService;
|
||||
use App\Services\TemplateService;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -23,7 +24,7 @@ class TemplatePreviewController extends Controller
|
||||
$type = $request->input('type', 'section'); // header, footer, section
|
||||
|
||||
// Validate type
|
||||
if (!in_array($type, ['header', 'footer', 'section'])) {
|
||||
if (!in_array($type, ['header', 'footer', 'section', 'menu'])) {
|
||||
$type = 'section';
|
||||
}
|
||||
|
||||
@@ -70,6 +71,7 @@ class TemplatePreviewController extends Controller
|
||||
'header' => HeaderTemplate::find($id),
|
||||
'footer' => FooterTemplate::find($id),
|
||||
'section' => SectionTemplate::find($id),
|
||||
'menu' => MenuTemplate::find($id),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class MenuTemplate extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'html_content',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Page;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class MenuService
|
||||
{
|
||||
/**
|
||||
* Render menu structure as HTML
|
||||
*
|
||||
* @param array|null $menuTemplate Optional menu template data
|
||||
* @return string Rendered menu HTML
|
||||
*/
|
||||
public static function render(?array $menuTemplate = null): string
|
||||
{
|
||||
// Get menu items from Pages
|
||||
$menuItems = Cache::remember('menu_items', 3600, function () {
|
||||
return Page::with(['children' => function ($query) {
|
||||
$query->where('status', 'published')
|
||||
->where('show_in_menu', true)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->orderBy('title', 'asc');
|
||||
}])
|
||||
->whereNull('parent_id')
|
||||
->where('status', 'published')
|
||||
->where('show_in_menu', true)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->orderBy('title', 'asc')
|
||||
->get();
|
||||
});
|
||||
|
||||
// If menu template is provided, use it to wrap the menu
|
||||
if ($menuTemplate && !empty($menuTemplate['html_content'])) {
|
||||
$html = $menuTemplate['html_content'];
|
||||
|
||||
// Replace {menu} placeholder with rendered menu structure
|
||||
$renderedMenu = self::buildMenuStructure($menuItems);
|
||||
$html = str_replace('{menu}', $renderedMenu, $html);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Otherwise, return default menu structure
|
||||
return self::buildMenuStructure($menuItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build menu structure HTML from menu items
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $menuItems
|
||||
* @return string HTML structure
|
||||
*/
|
||||
protected static function buildMenuStructure($menuItems): string
|
||||
{
|
||||
if ($menuItems->isEmpty()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '<ul class="menu">';
|
||||
|
||||
foreach ($menuItems as $item) {
|
||||
$hasChildren = $item->children->isNotEmpty();
|
||||
|
||||
$html .= '<li class="menu-item">';
|
||||
$html .= '<a href="' . route('page.show', $item->slug) . '" class="menu-link">';
|
||||
$html .= e($item->title);
|
||||
$html .= '</a>';
|
||||
|
||||
if ($hasChildren) {
|
||||
$html .= self::buildSubmenu($item->children);
|
||||
}
|
||||
|
||||
$html .= '</li>';
|
||||
}
|
||||
|
||||
$html .= '</ul>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build submenu structure recursively
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $children
|
||||
* @return string Submenu HTML
|
||||
*/
|
||||
protected static function buildSubmenu($children): string
|
||||
{
|
||||
$html = '<ul class="submenu">';
|
||||
|
||||
foreach ($children as $child) {
|
||||
$hasChildren = $child->children->isNotEmpty();
|
||||
|
||||
$html .= '<li class="submenu-item">';
|
||||
$html .= '<a href="' . route('page.show', $child->slug) . '" class="submenu-link">';
|
||||
$html .= e($child->title);
|
||||
$html .= '</a>';
|
||||
|
||||
if ($hasChildren) {
|
||||
$html .= self::buildSubmenu($child->children);
|
||||
}
|
||||
|
||||
$html .= '</li>';
|
||||
}
|
||||
|
||||
$html .= '</ul>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear menu cache
|
||||
*/
|
||||
public static function clearCache(): void
|
||||
{
|
||||
Cache::forget('menu_items');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,7 @@ class TemplatePreviewService
|
||||
$bodyContent = match($type) {
|
||||
'header' => $content,
|
||||
'footer' => $content,
|
||||
'menu' => $content,
|
||||
'section' => '<div class="container py-8">' . $content . '</div>',
|
||||
default => $content,
|
||||
};
|
||||
|
||||
@@ -267,9 +267,16 @@ class TemplateService
|
||||
/**
|
||||
* Replace placeholders in HTML with actual data
|
||||
* Supports both {text.title} and {{text.title}} formats
|
||||
* Also supports {menu} placeholder for menu rendering
|
||||
*/
|
||||
public static function replacePlaceholders(string $html, array $data): string
|
||||
{
|
||||
// Handle special {menu} placeholder first
|
||||
if (str_contains($html, '{menu}')) {
|
||||
$renderedMenu = \App\Services\MenuService::render();
|
||||
$html = str_replace('{menu}', $renderedMenu, $html);
|
||||
}
|
||||
|
||||
// First, parse all placeholders in the format {type.field_name}
|
||||
preg_match_all('/\{([a-z]+\.[a-z_]+)\}/i', $html, $matches);
|
||||
$placeholders = array_unique($matches[1] ?? []);
|
||||
|
||||
Reference in New Issue
Block a user