Всем привет! Расскажу как кастомизировать страницу формы в MoonShine 2.0, а именно сделаем вкладки для полей HasMany c помощью декораций Tabs и Tab, а в качестве бонуса, нарисуем отдельную кнопку, в которой мы будем загружать множетсво изображений и отправлять их на добавление для одной из связей. В итоге у нас получится следующая страница:
Итак дано: PostResource, и его HasMany отношения PostCommentResource и PostImageResource.
class PostResource extends ModelResource
{
public string $model = Post::class;
public string $title = 'Posts';
public array $with = ['comments', 'images'];
public function fields(): array
{
return [
ID::make(),
Text::make('Name'),
Text::make('Content'),
HasMany::make('Comments', resource: new PostCommentResource())->creatable(),
HasMany::make('Images', resource: new PostImageResource())->creatable()
];
}
public function rules(Model $item): array
{
return [
'name' => 'required',
'content' => 'required'
];
}
}
Страница формы в MoonShine 2.0 будет иметь следующий вид:
Задача состоит в том, чтобы отправить форму редактирования и связи в отдельные вкладки (tabs). Чтобы сделать новую страницу, нам необходимо переопределить метод pages у ресурса. Добавляем перед методом fields следующий код (не забываем делать импорты):
protected function pages(): array
{
return [
IndexPage::make($this->title()),
FormPage::make(
$this->getItemID()
? __('moonshine::ui.edit')
: __('moonshine::ui.add')
),
DetailPage::make(__('moonshine::ui.show')),
];
}
Теперь нам нужно заменить FormPage на собственную страницу. Создаем новую страницу: php artisan moonshine:page PostFormPage
и удаляем все методы, кроме components. Также необходимо сделать наследование базовой FormPage:
class PostFormPage extends FormPage
{
public function components(): array
{
return [];
}
}
Заменяем FormPage на PostFormPage:
protected function pages(): array
{
return [
IndexPage::make($this->title()),
PostFormPage::make(
$this->getItemID()
? __('moonshine::ui.edit')
: __('moonshine::ui.add')
),
DetailPage::make(__('moonshine::ui.show')),
];
}
Теперь нам осталось немного изменить структуру компонентов. В MoonShine 2.0 на базовых CRUD страницах имеются слои TopLayer, MainLayer, BottomLayer. На странице формы в верхнем слое находятся ActionButtons, в главном слое находится форма, и в нижнем слое находятся поля отношений. Соответсвенно идея состоит в том, чтобы "вытащить" из нижнего слоя все поля отношений и "рассортировать" их по нужным табам. Все компоненты отношений имеют соответствующие имена связей. Итоговый код:
class PostFormPage extends FormPage
{
public function components(): array
{
//$this->getResource()->getItemID() - id текущей записи PostResource
//если нет идентификтора, значит нам нужно стандартное поведение при добавлении записи
if(! $this->getResource()->getItemID()) {
return parent::components();
}
$bottomComponents = $this->getLayerComponents(Layer::BOTTOM);
//извлекаем компонент с изображениями
$imagesComponent = collect($bottomComponents)->filter(fn($component) => $component->getName() === 'images')->first();
//извлекаем компонент с комментариями
$commentsComponent = collect($bottomComponents)->filter(fn($component) => $component->getName() === 'comments')->first();
//сортируем по табам
$tabLayer = [
Block::make('', [
Tabs::make([
Tab::make('Редактирование', $this->mainLayer()),
Tab::make('Изображения', [$imagesComponent]),
Tab::make('Комментарии', [$commentsComponent])
])
])
];
return [
...$this->getLayerComponents(Layer::TOP),
...$tabLayer,
];
}
}
В результате получаем следующую страницу формы:
И в качестве бонуса. На последнем изображении вы можете видеть свзяь HasMany в которой хранятся изображения, по умолчанию в такую связь мы не можем загрузить одновременно несколько картинок. Для этого нам нужно создать свой контроллер, в который мы отправим несколько изображений, сохраним их в файловой системе и создадим записи в базе данных. Эту логику реализаций я оставлю на вас, но покажу как создать саму форму отправки изображений. Все кнопки в MoonShine 2.0 имеют класс ActionButton, так же мы взяли за практику оборачивать логику кнопок в отдельную обертку для чистоты кода. В каталоге MoonShine создайте каталог Buttons, в нем создайте следующий класс:
final class UploadImagesButton
{
public static function for(string $resourceItem): ActionButton
{
return ActionButton::make('Загрузка изображений')
->inModal(
fn() => 'Выберите изображения для загрузки',
fn() => (string) FormBuilder::make(route('<здесь ваш роут на обработку формы>'))
->fields(
[
File::make('', 'file_images')->multiple(),
Hidden::make('resource_item')->setValue($resourceItem),
]
)
//делаем загрузку асинхронной,
//таблица с изображениями обновится после обработки формы
->async(asyncEvents: 'table-updated-images')
->submit('Загрузить')
)
;
}
}
По нажатию на кнопку "Загрузка изображений" у нас появится модальное окно с формой.
Добавляем эту кнопку на страницу.
$uploadButton = UploadImagesButton::for($this->getResource()->getItemID());
$imagesComponent = collect($bottomComponents)->filter(fn($component) => $component->getName() === 'images')->first();
$commentsComponent = collect($bottomComponents)->filter(fn($component) => $component->getName() === 'comments')->first();
$tabLayer = [
Block::make('', [
Tabs::make([
Tab::make('Редактирование', $this->mainLayer()),
Tab::make('Изображения', [$uploadButton, $imagesComponent]),
Tab::make('Комментарии', [$commentsComponent])
])
])
];
У поля HasMany Images в ресурсe PostResource убираем метод createable() и title
HasMany::make('', 'images', resource: new PostImageResource())
В итоге мы получаем страницу формы с табами и новым функционалом для загрузки изображений, как на первом изображении в этой статье.
Vasili Khlystou