Skip to Content
ModulesAdmin Panel

Flute CMS Admin Panel

Flute CMS provides a powerful and flexible system for creating administrative interfaces. The system is built on Packages, Screens, and Layouts, allowing you to quickly create complex CRUD interfaces with minimal code.

Architecture

The admin panel consists of several key components:

  • AdminPanel — main class managing package initialization and registration
  • AdminPackageFactory — factory for loading and initializing packages
  • AbstractAdminPackage — base class for creating packages
  • Screen — base class for creating screens
  • LayoutFactory — factory for creating layouts
  • Field — base class for form fields
┌─────────────────────────────────────────────────────────┐ │ AdminPanel │ │ ┌─────────────────────────────────────────────────┐ │ │ │ AdminPackageFactory │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ Package │ │ Package │ │ Package │ ... │ │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ │ │ │ │ Screen │ │ Screen │ │ Screen │ ... │ │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ ┌────▼────────────▼──────────▼────┐ │ │ │ │ │ Layouts │ │ │ │ │ │ (Table, Rows, Tabs, Modal...) │ │ │ │ │ └─────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘

Creating an Admin Package

Package Structure

    • MyModuleAdminPackage.php
      • ItemListScreen.php
      • ItemEditScreen.php
    • routes.php

Base Package

All admin packages inherit from AbstractAdminPackage:

<?php namespace Flute\Modules\MyModule\Admin; use Flute\Admin\Support\AbstractAdminPackage; class MyModuleAdminPackage extends AbstractAdminPackage { public function initialize(): void { parent::initialize(); // Load routes from file $this->loadRoutesFromFile('routes.php'); // Register views with namespace $this->loadViews('Resources/views', 'admin-mymodule'); // Load translations $this->loadTranslations('Resources/lang'); // Register SCSS for admin panel $this->registerScss('Resources/assets/scss/admin.scss'); } public function getPermissions(): array { return ['admin', 'admin.mymodule']; } public function getMenuItems(): array { return [ [ 'type' => 'header', 'title' => __('admin-mymodule.menu.header'), ], [ 'title' => __('admin-mymodule.menu.items'), 'icon' => 'ph.bold.list-bold', 'url' => url('/admin/mymodule'), 'permission' => 'admin.mymodule', ], ]; } public function getPriority(): int { return 50; // Lower value = higher in the list } }

AbstractAdminPackage Methods

MethodDescription
initialize()Package initialization: registering routes, views, translations
boot()Additional logic after all packages are initialized
getPermissions()Array of permissions required to access the package
getMenuItems()Array of menu items
getPriority()Package priority (loading and menu order)
loadRoutesFromFile(string $file)Load routes from file
loadRoutes(array $routes)Programmatic route loading
loadViews(string $path, string $namespace)Register views
loadTranslations(string $langDir)Load translations
registerScss(string $path)Register SCSS file for admin panel
getBasePath()Get base path of the package

Menu items support the following parameters:

public function getMenuItems(): array { return [ // Section header [ 'type' => 'header', 'title' => 'Menu Section', ], // Simple item [ 'title' => 'Menu Item', 'icon' => 'ph.bold.house-bold', 'url' => url('/admin/path'), 'permission' => 'admin.permission', ], // Item with nested items [ 'title' => 'Parent Item', 'icon' => 'ph.bold.folder-bold', 'permission' => 'admin.parent', 'children' => [ [ 'title' => 'Child Item 1', 'icon' => 'ph.regular.file', 'url' => url('/admin/parent/child1'), ], [ 'title' => 'Child Item 2', 'icon' => 'ph.regular.file', 'url' => url('/admin/parent/child2'), ], ], ], // Item with permission check mode [ 'title' => 'Special Item', 'icon' => 'ph.bold.shield-bold', 'url' => url('/admin/special'), 'permission' => ['admin.perm1', 'admin.perm2'], 'permission_mode' => 'any', // 'all' (default) or 'any' ], ]; }

Icons use Phosphor Icons format: ph.{weight}.{name}, where weight is regular, bold, fill, duotone, thin, light.

Routing

routes.php File

<?php use Flute\Core\Router\Router; use Flute\Modules\MyModule\Admin\Screens\ItemListScreen; use Flute\Modules\MyModule\Admin\Screens\ItemEditScreen; // Register screens Router::screen('/admin/mymodule', ItemListScreen::class); Router::screen('/admin/mymodule/{id}/edit', ItemEditScreen::class);

The Router::screen() macro automatically:

  • Adds can:admin middleware
  • Wraps the component in admin::layouts.screen layout
  • Supports HTMX/Yoyo requests

Programmatic Route Registration

public function initialize(): void { // Direct screen registration router()->screen('/admin/mymodule', ItemListScreen::class); // Or via loadRoutes $this->loadRoutes([ [ 'method' => 'GET', 'uri' => '/admin/mymodule/api/items', 'action' => [ItemController::class, 'list'], ], ]); }

Screens

Screens are the main building blocks of the admin panel interface. Each screen is a Yoyo component with reactivity support.

Basic Screen

<?php namespace Flute\Modules\MyModule\Admin\Screens; use Flute\Admin\Platform\Screen; use Flute\Admin\Platform\Layouts\LayoutFactory; class ItemListScreen extends Screen { public ?string $name = 'admin-mymodule.screen.list.title'; public ?string $description = 'admin-mymodule.screen.list.description'; public ?string $permission = 'admin.mymodule'; public $items; public function mount(): void { breadcrumb() ->add(__('def.admin_panel'), url('/admin')) ->add(__('admin-mymodule.breadcrumbs.items')); $this->items = rep(Item::class)->select()->orderBy('id', 'DESC'); } public function layout(): array { return [ // Screen layouts ]; } public function commandBar(): array { return [ // Buttons in screen header ]; } }

Screen Properties

PropertyTypeDescription
$name?stringScreen title (supports translation keys)
$description?stringScreen description
$permission?string|arrayRequired permissions
$jsarrayJavaScript files to load
$cssarrayCSS files to load

Lifecycle Methods

MethodDescription
mount()Screen data initialization (called on first render)
layout()Returns array of layouts to display
commandBar()Returns array of actions in screen header
render()Screen rendering (usually not overridden)

Methods for Working with Data

class ItemEditScreen extends Screen { public $item; public function mount(): void { $id = request()->route('id'); $this->item = Item::findByPK($id); if (!$this->item) { $this->flashMessage(__('admin-mymodule.messages.not_found'), 'error'); $this->redirectTo('/admin/mymodule'); } } // Action handler method public function save(): void { $data = request()->input(); $validation = $this->validate([ 'title' => ['required', 'string', 'max-str-len:255'], 'content' => ['required', 'string'], ], $data); if (!$validation) { return; // Validation errors are automatically displayed } $this->item->title = $data['title']; $this->item->content = $data['content']; $this->item->save(); $this->flashMessage(__('admin-mymodule.messages.saved'), 'success'); } // Delete method public function delete(): void { $id = request()->input('id'); $item = Item::findByPK($id); if ($item) { $item->delete(); $this->flashMessage(__('admin-mymodule.messages.deleted'), 'success'); } } // Bulk delete public function bulkDelete(): void { $ids = request()->input('selected', []); foreach ($ids as $id) { $item = Item::findByPK((int) $id); if ($item) { $item->delete(); } } $this->flashMessage(__('admin-mymodule.messages.bulk_deleted'), 'success'); } }

Screens support modal windows via special methods:

class ItemListScreen extends Screen { public function commandBar(): array { return [ Button::make(__('admin-mymodule.buttons.create')) ->icon('ph.bold.plus-bold') ->modal('createModal') ->type(Color::PRIMARY), ]; } // Modal window method public function createModal(Repository $parameters) { return LayoutFactory::modal($parameters, [ LayoutFactory::field( Input::make('title') ->type('text') ->placeholder(__('admin-mymodule.fields.title.placeholder')) ) ->label(__('admin-mymodule.fields.title.label')) ->required(), LayoutFactory::field( TextArea::make('content') ->rows(5) ) ->label(__('admin-mymodule.fields.content.label')), ]) ->title(__('admin-mymodule.modal.create.title')) ->applyButton(__('admin-mymodule.modal.create.submit')) ->method('saveItem'); } // Modal window with parameters public function editModal(Repository $parameters) { $itemId = $parameters->get('id'); $item = Item::findByPK($itemId); if (!$item) { $this->flashMessage(__('admin-mymodule.messages.not_found'), 'error'); return; } return LayoutFactory::modal($parameters, [ LayoutFactory::field( Input::make('title') ->value($item->title) ) ->label(__('admin-mymodule.fields.title.label')) ->required(), ]) ->title(__('admin-mymodule.modal.edit.title')) ->applyButton(__('admin-mymodule.modal.edit.submit')) ->method('updateItem'); } public function saveItem(): void { // Saving logic $this->closeModal(); $this->flashMessage(__('admin-mymodule.messages.created'), 'success'); } public function updateItem(): void { $itemId = $this->modalParams->get('id'); // Update logic $this->closeModal(); $this->flashMessage(__('admin-mymodule.messages.updated'), 'success'); } }

Layouts

Layouts define the structure and display of content on the screen. All layouts are created via LayoutFactory.

Available Layouts

LayoutDescription
table()Table with data, pagination, sorting, and search
rows()Vertical group of form fields
columns()Horizontal arrangement of elements
split()Split into two columns
tabs()Tabs
modal()Modal window
block()Wrapper block
view()Custom Blade template
field()Single field with label
metrics()Metrics block
chart()Chart
sortable()Sortable list
blank()Empty container
wrapper()Custom wrapper

Table

use Flute\Admin\Platform\Fields\TD; use Flute\Admin\Platform\Actions\Button; use Flute\Admin\Platform\Actions\DropDown; use Flute\Admin\Platform\Actions\DropDownItem; use Flute\Admin\Platform\Support\Color; public function layout(): array { return [ LayoutFactory::table('items', [ // Selection column (checkboxes) TD::selection('id'), // Regular column TD::make('title', __('admin-mymodule.table.title')) ->sort() ->cantHide() ->minWidth('200px'), // Column with custom render TD::make('status', __('admin-mymodule.table.status')) ->render(fn (Item $item) => view('admin-mymodule::cells.status', compact('item'))) ->width('120px') ->alignCenter(), // Column with date TD::make('createdAt', __('admin-mymodule.table.created_at')) ->asComponent(\Flute\Admin\Platform\Components\Cells\DateTime::class) ->sort() ->defaultSort(true, 'desc') ->width('150px'), // Actions column TD::make('actions', __('admin-mymodule.table.actions')) ->class('actions-col') ->alignCenter() ->cantHide() ->disableSearch() ->width('100px') ->render(fn (Item $item) => DropDown::make() ->icon('ph.regular.dots-three-outline-vertical') ->list([ DropDownItem::make(__('admin-mymodule.buttons.edit')) ->redirect(url('/admin/mymodule/' . $item->id . '/edit')) ->icon('ph.bold.pencil-bold') ->type(Color::OUTLINE_PRIMARY) ->size('small') ->fullWidth(), DropDownItem::make(__('admin-mymodule.buttons.delete')) ->confirm(__('admin-mymodule.confirms.delete')) ->method('delete', ['id' => $item->id]) ->icon('ph.bold.trash-bold') ->type(Color::OUTLINE_DANGER) ->size('small') ->fullWidth(), ])), ]) ->title(__('admin-mymodule.table.title')) ->description(__('admin-mymodule.table.description')) ->searchable(['title', 'description']) ->perPage(15) ->compact() ->commands([ Button::make(__('admin-mymodule.buttons.create')) ->icon('ph.bold.plus-bold') ->redirect(url('/admin/mymodule/create')), ]) ->bulkActions([ Button::make(__('admin.bulk.delete_selected')) ->icon('ph.bold.trash-bold') ->type(Color::OUTLINE_DANGER) ->confirm(__('admin.confirms.delete_selected')) ->method('bulkDelete'), ]), ]; }

TD (Table Data) Methods

MethodDescription
make(string $name, ?string $title)Create column
selection(string $name)Column with checkboxes
render(callable $callback)Custom render
asComponent(string $class)Render via component
sort(bool $enabled)Enable sorting
defaultSort(bool $enabled, string $direction)Default sorting
searchable(bool $enabled)Enable search by column
disableSearch()Disable search
width(string $width)Column width
minWidth(string $width)Minimum width
align(string $align)Alignment: start, center, end
alignCenter() / alignRight()Quick alignment
cantHide()Prevent hiding column
defaultHidden(bool $hidden)Hidden by default
class(string $class)CSS class
style(string $style)Inline styles

Table Methods

MethodDescription
searchable(?array $columns)Enable search
setSearchableColumns(array $columns)Set columns for search
perPage(int $count)Records per page
title(?string $title)Table title
description(?string $description)Description
compact(bool $compact)Compact mode
commands(array $actions)Buttons above table
bulkActions(array $actions)Bulk actions
prepareContent(callable $callback)Row processing
dataCallback(callable $callback)Processing entire dataset

Rows — Group of Fields

public function layout(): array { return [ LayoutFactory::rows([ Input::make('title') ->type('text') ->placeholder(__('admin-mymodule.fields.title.placeholder')) ->required(), TextArea::make('description') ->rows(3), Select::make('category_id') ->options($this->categories) ->empty(__('admin-mymodule.fields.category.empty')), Toggle::make('is_active') ->label(__('admin-mymodule.fields.is_active.label')) ->checked($this->item?->is_active ?? true), ]) ->title(__('admin-mymodule.sections.main')) ->description(__('admin-mymodule.sections.main_description')), ]; }

Columns

public function layout(): array { return [ LayoutFactory::columns([ LayoutFactory::rows([ Input::make('title')->type('text'), TextArea::make('content'), ])->title(__('admin-mymodule.sections.content')), LayoutFactory::rows([ Select::make('status')->options($this->statuses), Input::make('published_at')->type('datetime-local'), ])->title(__('admin-mymodule.sections.settings')), ]), ]; }

Tabs

use Flute\Admin\Platform\Fields\Tab; public function layout(): array { return [ LayoutFactory::tabs([ Tab::make(__('admin-mymodule.tabs.general')) ->badge($this->items->count()) ->icon('ph.regular.info') ->layouts([ LayoutFactory::rows([ Input::make('title'), TextArea::make('description'), ]), ]), Tab::make(__('admin-mymodule.tabs.seo')) ->icon('ph.regular.magnifying-glass') ->layouts([ LayoutFactory::rows([ Input::make('meta_title'), TextArea::make('meta_description'), ]), ]), Tab::make(__('admin-mymodule.tabs.advanced')) ->layouts([ LayoutFactory::view('admin-mymodule::partials.advanced', [ 'item' => $this->item, ]), ]), ]) ->slug('item_tabs') ->pills() // Pills style instead of tabs ->sticky() // Sticky tabs ->lazyload() // Lazy content loading ->morph(false), // Disable animation ]; }
public function editModal(Repository $parameters) { return LayoutFactory::modal($parameters, [ LayoutFactory::field( Input::make('title')->value($parameters->get('title')) )->label(__('admin-mymodule.fields.title.label'))->required(), LayoutFactory::field( Select::make('status') ->options(['draft' => 'Draft', 'published' => 'Published']) ->value($parameters->get('status')) )->label(__('admin-mymodule.fields.status.label')), ]) ->title(__('admin-mymodule.modal.edit.title')) ->applyButton(__('def.save')) ->closeButton(__('def.cancel')) ->method('updateItem') ->size(Modal::SIZE_LG) // 'sm', 'lg', 'xl' ->right() // Open from right ->withoutApplyButton() // Without apply button ->withoutCloseButton() // Without close button ->removeOnClose(); // Remove DOM on close }

Metrics

public function layout(): array { return [ LayoutFactory::metrics([ __('admin-mymodule.metrics.total') => 'totalItems', __('admin-mymodule.metrics.active') => 'activeItems', __('admin-mymodule.metrics.views') => 'totalViews', ]) ->title(__('admin-mymodule.metrics.title')) ->setIcons([ __('admin-mymodule.metrics.total') => 'ph.regular.list', __('admin-mymodule.metrics.active') => 'ph.regular.check-circle', __('admin-mymodule.metrics.views') => 'ph.regular.eye', ]), ]; } public function mount(): void { $this->totalItems = Item::query()->count(); $this->activeItems = Item::query()->where('is_active', true)->count(); $this->totalViews = Item::query()->sum('views'); }

Chart

use Flute\Core\Charts\FluteChart; public function layout(): array { return [ LayoutFactory::chart('viewsChart', __('admin-mymodule.charts.views')) ->type('line') // 'line', 'bar', 'donut', 'area' ->height(300) ->colors(['#4F46E5', '#10B981']) ->labels($this->chartLabels) ->dataset($this->chartData), // Or from ready FluteChart object LayoutFactory::chart('salesChart') ->from($this->salesChart) ->title(__('admin-mymodule.charts.sales')) ->description(__('admin-mymodule.charts.sales_description')), ]; } public function mount(): void { $this->chartLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'May']; $this->chartData = [ ['name' => 'Views', 'data' => [100, 150, 200, 180, 250]], ['name' => 'Unique', 'data' => [50, 80, 120, 100, 150]], ]; $this->salesChart = (new FluteChart()) ->setType('bar') ->setLabels(['Q1', 'Q2', 'Q3', 'Q4']) ->setDataset([1200, 1500, 1800, 2100]); }

Sortable

use Flute\Admin\Platform\Fields\Sight; public function layout(): array { return [ LayoutFactory::sortable('items', [ Sight::make()->render(fn (Item $item) => view('admin-mymodule::cells.item', compact('item'))), Sight::make('actions', __('admin-mymodule.table.actions')) ->render(fn (Item $item) => DropDown::make() ->icon('ph.regular.dots-three-outline-vertical') ->list([ DropDownItem::make(__('def.edit')) ->modal('editModal', ['id' => $item->id]), DropDownItem::make(__('def.delete')) ->confirm(__('admin.confirms.delete')) ->method('delete', ['id' => $item->id]), ])), ])->onSortEnd('saveSortOrder'), ]; } public function saveSortOrder(): void { $result = json_decode(request()->input('sortableResult', '[]'), true); foreach (array_reverse($result) as $index => $itemData) { $item = Item::findByPK((int) $itemData['id']); if ($item) { $item->position = $index; $item->save(); } } orm()->getHeap()->clean(); $this->loadItems(); }

View — Custom Template

public function layout(): array { return [ LayoutFactory::view('admin-mymodule::partials.custom-section', [ 'items' => $this->items, 'stats' => $this->stats, ]), ]; }

Form Fields (Fields)

Input — Text Field

Input::make('title') ->type('text') // text, email, password, number, date, datetime-local, color, icon, file ->name('custom_name') ->value('Default value') ->placeholder('Enter title') ->required() ->disabled() ->readOnly() ->mask('99.99.9999') // Input mask ->prefix('$') // Prefix ->postPrefix('/month') // Postfix ->size('medium') // small, medium, large ->tooltip('Tooltip') ->datalist(['Option 1', 'Option 2']) // List of hints ->popover('Additional info'); // File field with FilePond Input::make('image') ->type('file') ->filePond() ->filePondOptions([ 'acceptedFileTypes' => ['image/*'], 'maxFileSize' => '2MB', ]) ->defaultFile($this->item?->image) ->multiple(); // Icon selection field Input::make('icon') ->type('icon') ->iconPacks(['phosphor', 'material']); // Color selection field Input::make('color') ->type('color') ->value('#4F46E5');

Select — Dropdown List

// Simple select Select::make('category_id') ->options([ 1 => 'Category 1', 2 => 'Category 2', 3 => 'Category 3', ]) ->value($this->item?->category_id) ->empty('Select category', '') ->required(); // Multiple select Select::make('tags') ->options($this->allTags) ->value($this->item?->tags?->pluck('id')->toArray()) ->multiple() ->maxItems(5) ->removeButton() ->clearButton(); // Select with database search Select::make('user_id') ->fromDatabase('users', 'name', 'id', ['name', 'email']) ->searchable(true, 2, 300) // minLength, delay ->preload() ->limit(20); // Select from Enum Select::make('status') ->fromEnum(ItemStatus::class, 'label') ->value($this->item?->status); // Select with ability to add Select::make('tags') ->options($this->tags) ->multiple() ->allowAdd() ->taggable();

TextArea — Multiline Field

TextArea::make('content') ->rows(5) ->placeholder('Enter text...') ->required();

Toggle — Switch

Toggle::make('is_active') ->label('Active') ->checked($this->item?->is_active ?? true) ->sendTrueOrFalse() // Send true/false instead of 1/0 ->yesvalue('active') // Value when on ->novalue('inactive') // Value when off ->disabled() ->yoyo(); // Reactive update

CheckBox

CheckBox::make('terms') ->label('I agree to terms') ->checked(true) ->popover('Required field'); // Group of checkboxes foreach ($permissions as $id => $name) { LayoutFactory::field( CheckBox::make("permissions.{$name}") ->label($name) ->checked($role->hasPermission($id)) ); }

Radio — Radio Buttons

Radio::make('type') ->options([ 'post' => 'Post', 'page' => 'Page', 'news' => 'News', ]) ->value($this->item?->type ?? 'post');

RichText — Editor

RichText::make('content') ->value($this->item?->content) ->placeholder('Start typing...');

Group — Field Group

Group::make([ Input::make('first_name')->placeholder('First Name'), Input::make('last_name')->placeholder('Last Name'), ])->title('Full Name');

Label — Information Label

Label::make('info') ->title('Important Information') ->value('This item will be deleted in 24 hours');

ViewField — Custom Field

ViewField::make('custom') ->view('admin-mymodule::fields.custom-field') ->with(['data' => $this->customData]);

Field Wrapper

LayoutFactory::field( Input::make('email')->type('email') ) ->label('Email Address') ->required() ->small('Will be used for notifications') ->popover('Email must be unique');

Actions

Button

use Flute\Admin\Platform\Actions\Button; use Flute\Admin\Platform\Support\Color; // Simple button with redirect Button::make(__('admin.buttons.create')) ->icon('ph.bold.plus-bold') ->redirect(url('/admin/items/create')) ->type(Color::PRIMARY); // Button calling method Button::make(__('admin.buttons.save')) ->icon('ph.bold.floppy-disk-bold') ->method('save') ->type(Color::SUCCESS); // Button with confirmation Button::make(__('admin.buttons.delete')) ->icon('ph.bold.trash-bold') ->method('delete', ['id' => $item->id]) ->confirm(__('admin.confirms.delete'), 'error') ->type(Color::DANGER); // Button opening modal Button::make(__('admin.buttons.edit')) ->icon('ph.bold.pencil-bold') ->modal('editModal', ['id' => $item->id]) ->type(Color::OUTLINE_PRIMARY); // Link button Button::make(__('admin.buttons.preview')) ->icon('ph.bold.eye-bold') ->href(url('/items/' . $item->slug)) ->type(Color::OUTLINE_SECONDARY); // Additional settings Button::make('Custom') ->size('small') // small, medium, large ->fullWidth() // Full width ->disabled() // Disabled ->tooltip('Tooltip') ->withLoading(true) // Loading indicator ->addClass('custom-class') ->novalidate(); // No form validation

Button Types (Color)

use Flute\Admin\Platform\Support\Color; Color::PRIMARY // Primary Color::SECONDARY // Secondary Color::SUCCESS // Success Color::DANGER // Danger Color::WARNING // Warning Color::INFO // Info Color::LIGHT // Light Color::DARK // Dark Color::OUTLINE_PRIMARY // Outline Primary Color::OUTLINE_SECONDARY Color::OUTLINE_SUCCESS Color::OUTLINE_DANGER Color::OUTLINE_WARNING Color::OUTLINE_INFO
use Flute\Admin\Platform\Actions\DropDown; use Flute\Admin\Platform\Actions\DropDownItem; DropDown::make() ->icon('ph.regular.dots-three-outline-vertical') ->list([ DropDownItem::make(__('admin.buttons.view')) ->redirect(url('/items/' . $item->id)) ->icon('ph.regular.eye') ->type(Color::OUTLINE_PRIMARY) ->size('small') ->fullWidth(), DropDownItem::make(__('admin.buttons.edit')) ->modal('editModal', ['id' => $item->id]) ->icon('ph.regular.pencil') ->type(Color::OUTLINE_PRIMARY) ->size('small') ->fullWidth(), DropDownItem::make(__('admin.buttons.duplicate')) ->method('duplicate', ['id' => $item->id]) ->icon('ph.regular.copy') ->type(Color::OUTLINE_SECONDARY) ->size('small') ->fullWidth(), DropDownItem::make(__('admin.buttons.delete')) ->confirm(__('admin.confirms.delete')) ->method('delete', ['id' => $item->id]) ->icon('ph.regular.trash') ->type(Color::OUTLINE_DANGER) ->size('small') ->fullWidth(), ]);

Module Integration

Registering in ModuleServiceProvider

<?php namespace Flute\Modules\MyModule\Providers; use Flute\Core\Support\ModuleServiceProvider; use Flute\Modules\MyModule\Admin\MyModuleAdminPackage; class MyModuleServiceProvider extends ModuleServiceProvider { public function boot(\DI\Container $container): void { $this->loadEntities(); $this->loadConfigs(); $this->loadTranslations(); $this->loadRouterAttributes(); $this->loadViews('Resources/views', 'mymodule'); // Register admin package only if accessing admin area if (is_admin_path() && user()->can('admin')) { $this->loadPackage(new MyModuleAdminPackage()); } } public function extensions(): array { return []; } }

Always check is_admin_path() and user()->can('admin') before loading admin package for performance optimization.

Events

The admin panel generates the following events:

PackageRegisteredEvent

Fires when a package is registered:

use Flute\Admin\Events\PackageRegisteredEvent; events()->addListener(PackageRegisteredEvent::NAME, function (PackageRegisteredEvent $event) { $package = $event->getPackage(); // Logic after package registration });

PackageInitializedEvent

Fires after package initialization:

use Flute\Admin\Events\PackageInitializedEvent; events()->addListener(PackageInitializedEvent::NAME, function (PackageInitializedEvent $event) { $package = $event->getPackage(); // Logic after initialization });

Table Cell Components

Components are used to format data in tables:

use Flute\Admin\Platform\Components\Cells\DateTime; use Flute\Admin\Platform\Components\Cells\DateTimeSplit; use Flute\Admin\Platform\Components\Cells\Boolean; use Flute\Admin\Platform\Components\Cells\Currency; use Flute\Admin\Platform\Components\Cells\Number; use Flute\Admin\Platform\Components\Cells\Percentage; use Flute\Admin\Platform\Components\Cells\BadgeLink; TD::make('createdAt')->asComponent(DateTime::class); TD::make('is_active')->asComponent(Boolean::class); TD::make('price')->asComponent(Currency::class); TD::make('views')->asComponent(Number::class); TD::make('progress')->asComponent(Percentage::class);

Screen Utilities

Flash Messages

$this->flashMessage('Operation completed successfully', 'success'); $this->flashMessage('An error occurred', 'error'); $this->flashMessage('Warning', 'warning'); $this->flashMessage('Information', 'info');

Redirect

$this->redirectTo('/admin/items'); $this->redirect('admin.items.list'); // By route name
$this->openModal('editModal', ['id' => 123]); $this->closeModal();

Validation

$validation = $this->validate([ 'title' => ['required', 'string', 'max-str-len:255'], 'email' => ['required', 'email', 'unique:users,email'], 'category_id' => ['required', 'integer', 'exists:categories,id'], ], request()->input()); if (!$validation) { return; // Errors are displayed automatically }

Loading JS/CSS

public function mount(): void { $this->loadJS('app/Modules/MyModule/Resources/assets/js/admin.js'); $this->loadCSS('app/Modules/MyModule/Resources/assets/css/admin.css'); }
public function mount(): void { breadcrumb() ->add(__('def.admin_panel'), url('/admin')) ->add(__('admin-mymodule.breadcrumbs.items'), url('/admin/mymodule')) ->add(__('admin-mymodule.breadcrumbs.edit')); }

Best Practices

Module Structure

Create Folder Structure

app/Modules/MyModule/ ├── Admin/ │ ├── MyModuleAdminPackage.php │ ├── Screens/ │ │ ├── ItemListScreen.php │ │ └── ItemEditScreen.php │ ├── routes.php │ └── Resources/ │ ├── views/ │ │ └── cells/ │ ├── assets/scss/ │ └── lang/

Create Package with Basic Configuration

Create Screens for CRUD Operations

Register Package in Module ServiceProvider

Performance

  • Use lazy loading (lazyload()) for tabs
  • Cache heavy queries in mount()
  • Use pagination for large lists
  • Load only necessary relations via load()

Security

  • Always specify $permission in screens
  • Check permissions in handler methods
  • Validate all incoming data
  • Use user()->can() to check actions on objects

UX

  • Use clear titles and descriptions
  • Add confirmations for destructive actions
  • Show flash messages about operation results
  • Use breadcrumbs for navigation
// Example of object permission check public function delete(): void { $item = Item::findByPK(request()->input('id')); if (!$item) { $this->flashMessage(__('admin.messages.not_found'), 'error'); return; } if (!user()->can($item)) { $this->flashMessage(__('admin.messages.access_denied'), 'error'); return; } $item->delete(); $this->flashMessage(__('admin.messages.deleted'), 'success'); }

Full CRUD Screen Example

<?php namespace Flute\Modules\Blog\Admin\Screens; use Flute\Admin\Platform\Actions\Button; use Flute\Admin\Platform\Actions\DropDown; use Flute\Admin\Platform\Actions\DropDownItem; use Flute\Admin\Platform\Fields\Input; use Flute\Admin\Platform\Fields\Select; use Flute\Admin\Platform\Fields\Tab; use Flute\Admin\Platform\Fields\TD; use Flute\Admin\Platform\Fields\TextArea; use Flute\Admin\Platform\Fields\Toggle; use Flute\Admin\Platform\Layouts\LayoutFactory; use Flute\Admin\Platform\Repository; use Flute\Admin\Platform\Screen; use Flute\Admin\Platform\Support\Color; use Flute\Modules\Blog\Database\Entities\Article; use Flute\Modules\Blog\Database\Entities\Category; class ArticleListScreen extends Screen { public ?string $name = 'blog.admin.articles.title'; public ?string $description = 'blog.admin.articles.description'; public ?string $permission = 'admin.blog'; public $articles; public $categories; public function mount(): void { breadcrumb() ->add(__('def.admin_panel'), url('/admin')) ->add(__('blog.admin.articles.title')); $this->articles = rep(Article::class) ->select() ->load('category') ->orderBy('createdAt', 'DESC'); $this->categories = collect(Category::findAll()) ->pluck('name', 'id') ->toArray(); } public function commandBar(): array { return [ Button::make(__('blog.admin.buttons.create')) ->icon('ph.bold.plus-bold') ->modal('createModal') ->type(Color::PRIMARY), ]; } public function layout(): array { return [ LayoutFactory::table('articles', [ TD::selection('id'), TD::make('title', __('blog.admin.table.title')) ->render(fn (Article $a) => view('admin-blog::cells.article', ['article' => $a])) ->sort() ->minWidth('250px'), TD::make('category.name', __('blog.admin.table.category')) ->width('150px'), TD::make('is_published', __('blog.admin.table.status')) ->render(fn (Article $a) => $a->is_published ? '<span class="badge bg-success">' . __('blog.admin.status.published') . '</span>' : '<span class="badge bg-secondary">' . __('blog.admin.status.draft') . '</span>') ->alignCenter() ->width('120px'), TD::make('views', __('blog.admin.table.views')) ->sort() ->alignCenter() ->width('100px'), TD::make('createdAt', __('blog.admin.table.created')) ->asComponent(\Flute\Admin\Platform\Components\Cells\DateTime::class) ->sort() ->defaultSort(true, 'desc') ->width('150px'), TD::make('actions') ->alignCenter() ->cantHide() ->disableSearch() ->width('80px') ->render(fn (Article $a) => DropDown::make() ->icon('ph.regular.dots-three-outline-vertical') ->list([ DropDownItem::make(__('def.edit')) ->modal('editModal', ['id' => $a->id]) ->icon('ph.regular.pencil') ->type(Color::OUTLINE_PRIMARY) ->size('small') ->fullWidth(), DropDownItem::make(__('def.delete')) ->confirm(__('blog.admin.confirms.delete')) ->method('delete', ['id' => $a->id]) ->icon('ph.regular.trash') ->type(Color::OUTLINE_DANGER) ->size('small') ->fullWidth(), ])), ]) ->searchable(['title', 'content']) ->perPage(15) ->bulkActions([ Button::make(__('admin.bulk.delete_selected')) ->icon('ph.bold.trash-bold') ->type(Color::OUTLINE_DANGER) ->confirm(__('admin.confirms.delete_selected')) ->method('bulkDelete'), ]), ]; } public function createModal(Repository $params) { return LayoutFactory::modal($params, [ LayoutFactory::tabs([ Tab::make(__('blog.admin.tabs.content'))->layouts([ LayoutFactory::field( Input::make('title')->placeholder(__('blog.admin.fields.title.placeholder')) )->label(__('blog.admin.fields.title.label'))->required(), LayoutFactory::field( TextArea::make('content')->rows(10) )->label(__('blog.admin.fields.content.label'))->required(), ]), Tab::make(__('blog.admin.tabs.settings'))->layouts([ LayoutFactory::field( Select::make('category_id') ->options($this->categories) ->empty(__('blog.admin.fields.category.empty')) )->label(__('blog.admin.fields.category.label')), LayoutFactory::field( Toggle::make('is_published')->label(__('blog.admin.fields.published.label')) ), ]), ])->slug('create_article_tabs'), ]) ->title(__('blog.admin.modal.create.title')) ->applyButton(__('def.create')) ->method('store') ->size('lg'); } public function editModal(Repository $params) { $article = Article::findByPK($params->get('id')); if (!$article) { $this->flashMessage(__('blog.admin.messages.not_found'), 'error'); return; } return LayoutFactory::modal($params, [ LayoutFactory::tabs([ Tab::make(__('blog.admin.tabs.content'))->layouts([ LayoutFactory::field( Input::make('title')->value($article->title) )->label(__('blog.admin.fields.title.label'))->required(), LayoutFactory::field( TextArea::make('content')->rows(10)->value($article->content) )->label(__('blog.admin.fields.content.label'))->required(), ]), Tab::make(__('blog.admin.tabs.settings'))->layouts([ LayoutFactory::field( Select::make('category_id') ->options($this->categories) ->value($article->category_id) ->empty(__('blog.admin.fields.category.empty')) )->label(__('blog.admin.fields.category.label')), LayoutFactory::field( Toggle::make('is_published') ->checked($article->is_published) ->label(__('blog.admin.fields.published.label')) ), ]), ])->slug('edit_article_tabs'), ]) ->title(__('blog.admin.modal.edit.title')) ->applyButton(__('def.save')) ->method('update') ->size('lg'); } public function store(): void { $data = request()->input(); $valid = $this->validate([ 'title' => ['required', 'string', 'max-str-len:255'], 'content' => ['required', 'string'], 'category_id' => ['nullable', 'integer', 'exists:blog_categories,id'], ], $data); if (!$valid) return; $article = new Article(); $article->title = $data['title']; $article->content = $data['content']; $article->category_id = $data['category_id'] ?: null; $article->is_published = isset($data['is_published']); $article->author_id = user()->id; $article->save(); $this->closeModal(); $this->flashMessage(__('blog.admin.messages.created'), 'success'); $this->articles = rep(Article::class) ->select() ->load('category') ->orderBy('createdAt', 'DESC'); } public function update(): void { $id = $this->modalParams->get('id'); $article = Article::findByPK($id); if (!$article) { $this->flashMessage(__('blog.admin.messages.not_found'), 'error'); return; } $data = request()->input(); $valid = $this->validate([ 'title' => ['required', 'string', 'max-str-len:255'], 'content' => ['required', 'string'], 'category_id' => ['nullable', 'integer', 'exists:blog_categories,id'], ], $data); if (!$valid) return; $article->title = $data['title']; $article->content = $data['content']; $article->category_id = $data['category_id'] ?: null; $article->is_published = isset($data['is_published']); $article->save(); $this->closeModal(); $this->flashMessage(__('blog.admin.messages.updated'), 'success'); } public function delete(): void { $article = Article::findByPK(request()->input('id')); if ($article) { $article->delete(); $this->flashMessage(__('blog.admin.messages.deleted'), 'success'); } } public function bulkDelete(): void { $ids = request()->input('selected', []); foreach ($ids as $id) { $article = Article::findByPK((int) $id); $article?->delete(); } $this->flashMessage(__('blog.admin.messages.bulk_deleted'), 'success'); } }