Кастомизация страницы формы. MoonShine 2.0

Кастомизация страницы формы. MoonShine 2.0

Иван Левченко
Иван Левченко
04.11.2023 в 09:32

Всем привет! Расскажу как кастомизировать страницу формы в 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
Vasili Khlystou
15.11.2023 в 13:57
Отличный материал! Спасибо автору за труды!