ewk / moonshine-resource-kit
Drag & drop sorting and activity toggling toolkit for MoonShine 4 resources.
Requires
- php: >=8.3
- laravel/framework: ^12|^13
- moonshine/moonshine: ^4.15
Requires (Dev)
- driftingly/rector-laravel: ^2
- friendsofphp/php-cs-fixer: ^3.65
- larastan/larastan: ^3
- mockery/mockery: ^1.2
- orchestra/testbench: ^10|^11
- pestphp/pest: ^3
- pestphp/pest-plugin-arch: ^3
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11|^12
- rector/rector: ^2
- tomasvotruba/type-coverage: ^2.2
README
Тулкит для ресурсов MoonShine 4: drag & drop сортировка и переключатель активности.
Извлечён из паттернов, повторяющихся в реальных проектах: колонка sort_order с перетаскиванием строк в админской таблице и флаг is_active с переключением по одной записи и массово. Пакет — тонкий клей над встроенными механизмами MoonShine (reorderable()-таблицы на SortableJS, updateOnPreview() у переключателей): без собственного JS, всё настраивается на уровне модели/ресурса.
Требования
- PHP 8.3+
- Laravel 12/13
- MoonShine 4.15+
Установка
composer require ewk/moonshine-resource-kit
Сервис-провайдер подключается автоматически. При необходимости опубликуйте конфиг и переводы:
php artisan vendor:publish --tag=moonshine-resource-kit-config php artisan vendor:publish --tag=moonshine-resource-kit-lang
Сортировка
Модель
Добавьте колонку sort_order ($table->unsignedInteger('sort_order')->nullable();) и трейт:
use Ewk\MoonShineResourceKit\Concerns\Sortable; class Article extends Model { use Sortable; }
Новые записи получают позицию max + 1 автоматически. Также трейт даёт:
Article::query()->orderedBySort()->get(); // ... ->orderedBySortDesc() $article->moveUp(); $article->moveDown(); $article->moveToStart(); $article->moveToEnd(); $article->moveToPosition(7);
Позиции плотные (всегда 1..N), перемещение дешёвое: один UPDATE со сдвигом диапазона плюс один UPDATE с установкой позиции — независимо от размера таблицы.
Своя колонка — переопределите в модели (глобальный дефолт задаётся в конфиге):
public function getSortColumn(): string { return 'position'; }
Групповые последовательности — независимая сортировка внутри каждой группы (например, блоки внутри страницы):
public function getSortGroupColumns(): array { return ['page_id']; }
Ресурс
use Ewk\MoonShineResourceKit\MoonShine\Concerns\WithReorderable; class ArticleResource extends ModelResource { use WithReorderable; }
Дальше включите drag & drop на индексной странице — либо трейтом страницы:
use Ewk\MoonShineResourceKit\MoonShine\Concerns\ReorderableIndex; class ArticleIndexPage extends IndexPage { use ReorderableIndex; }
либо вручную в modifyListComponent():
protected function modifyListComponent(ComponentContract $component): ComponentContract { return $this->getResource()->applyReorderable($component, $this); }
Пагинация и большие таблицы
Drag & drop достаёт только до строк текущей страницы — и это нормально: когда таблица отсортирована по колонке сортировки, страница занимает непрерывный срез позиций, и пакет перераспределяет ровно этот срез. Остальные страницы не затрагиваются.
Чтобы переносить записи между страницами, добавьте кнопки перемещения на индексную страницу:
use Ewk\MoonShineResourceKit\MoonShine\Buttons\ReorderButtons; protected function buttons(): ListOf { return parent::buttons()->prepend(...ReorderButtons::for($this->getResource())); }
Вы получите «вверх / вниз», «в начало / в конец» и модалку «на позицию».
Каждое перемещение — два UPDATE, так что решение масштабируется на большие
таблицы без перезаписи всех строк.
Ручка перетаскивания
По умолчанию тянется вся строка. Чтобы перетаскивать только за ручку:
use Ewk\MoonShineResourceKit\MoonShine\Fields\SortHandle; protected function fields(): iterable { return [ SortHandle::make(), ID::make(), // ... ]; } protected function reorderableWithHandle(): bool // на индексной странице с ReorderableIndex { return true; }
Вложенные таблицы
Для таблиц HasMany, которые рендерятся внутри формы другого ресурса:
HasMany::make('Blocks', resource: BlockResource::class) ->modifyTable(app(BlockResource::class)->reorderableTableModifier()),
Переключатель активности
Модель
Добавьте boolean-колонку ($table->boolean('is_active')->default(true);) и трейт:
use Ewk\MoonShineResourceKit\Concerns\Activatable; class Article extends Model { use Activatable; }
Вы получите activate() / deactivate() / toggleActive() / isActive(),
скоупы active() / inactive() и boolean-каст. Чтобы использовать другую
колонку, переопределите getActiveColumn().
Ресурс
use Ewk\MoonShineResourceKit\MoonShine\Concerns\WithActivatable; class ArticleResource extends ModelResource { use WithActivatable; }
Переключение прямо из индексной таблицы:
use Ewk\MoonShineResourceKit\MoonShine\Fields\ActiveSwitcher; protected function fields(): iterable { return [ // ... ActiveSwitcher::make('Активность'), ]; }
Кнопки массовой активации/деактивации:
use Ewk\MoonShineResourceKit\MoonShine\Buttons\ActivityButtons; protected function buttons(): ListOf { return parent::buttons()->prepend(...ActivityButtons::for($this->getResource())); }
Глобальные скоупы (opt-in)
ActiveScope (скрывает неактивные записи) и SortOrderScope (неявная
сортировка) поставляются в пакете, но никогда не регистрируются
автоматически — глобальный скоуп, зависящий от текущего URL, стреляет в ногу
в очередях, Octane и artisan. Регистрируйте их там, где ограничение реально
нужно:
use Ewk\MoonShineResourceKit\Scopes\ActiveScope; protected static function booted(): void { static::addGlobalScope(new ActiveScope()); } // обходной путь: Article::query()->withInactive()->get();
Состояния фабрик
use Ewk\MoonShineResourceKit\Concerns\HasActiveFactoryStates; class ArticleFactory extends Factory { use HasActiveFactoryStates; } ArticleFactory::new()->active()->create(); ArticleFactory::new()->inactive()->create(); ArticleFactory::new()->randomActive()->create();
Конфигурация
config/moonshine-resource-kit.php:
| Ключ | По умолчанию | Назначение |
|---|---|---|
sort_column |
sort_order |
Глобальный дефолт для трейта Sortable |
active_column |
is_active |
Глобальный дефолт для трейта Activatable |
handle_class |
msrk-handle |
CSS-класс ячейки-ручки перетаскивания |
Переопределения на уровне модели (getSortColumn(), getSortGroupColumns(),
getActiveColumn()) всегда приоритетнее конфига.
Разработка
composer test # arch + unit + feature composer phpstan # level max, только src/ composer format # PHP-CS-Fixer (PER 3.0)