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
| Method | Description |
|---|---|
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 Configuration
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:adminmiddleware - Wraps the component in
admin::layouts.screenlayout - 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
| Property | Type | Description |
|---|---|---|
$name | ?string | Screen title (supports translation keys) |
$description | ?string | Screen description |
$permission | ?string|array | Required permissions |
$js | array | JavaScript files to load |
$css | array | CSS files to load |
Lifecycle Methods
| Method | Description |
|---|---|
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');
}
}Modal Windows
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
| Layout | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
];
}Modal
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 updateCheckBox
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 validationButton 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_INFODropDown
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 nameModal Windows
$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');
}Breadcrumbs
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
$permissionin 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');
}
}