Symfony 4 / Sonata: Tạo giao diện CRUD lồng nhau (con 1->N) với tính năng sắp xếp bằng kéo và thả

Ngắn gọn, chúng tôi muốn tạo một giao diện CRUD, 1N, với đó, khi chúng tôi đang chỉnh sửa một mục, chúng tôi thêm một bảng điều khiển để quản lý tất cả các mục con.

Tại đây chúng tôi có một bảng wtype, với một bảng wconf chứa một loạt bản ghi liên kết với một mục wtype.

Sélection_081
Sélection_082

Giống như ví dụ triển khai của sortable với kéo và thả ( có sẵn tại đây ) chúng tôi sẽ sử dụng các thành phần sau:
pixassociates/sortable-behavior-bundlestof/doctrine-extensions-bundle
Do đó bạn sẽ phải tạo trước một thực thể chứa mối quan hệ 1N với một thực thể thứ hai.

Thao tác chỉ bao gồm việc tiêm thực thể thứ hai vào lời gọi khai báo dịch vụ của thực thể đầu tiên với đối số “addChild” và tham chiếu đến dịch vụ cha mẹ.

Sélection_083

Khi chúng tôi muốn tham chiếu đến một dịch vụ, chúng tôi sử dụng chuỗi được sử dụng cho việc khai báo (tự do), và chúng tôi thêm “@” để chỉ ra rằng đó là một tham chiếu.

#config/services.yaml
services:
     admin.wconf:
        class: App\Admin\WconfAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, label: "Configuration des types" }
        public: true
        arguments: [~, App\Entity\Wconf, 'PixSortableBehaviorBundle:SortableAdmin']
        calls:
            - [setPositionService, ["@pix_sortable_behavior.position"]]
        
    admin.wtype:
        class: App\Admin\WtypeAdmin
        arguments: [~, App\Entity\Wtype, ~]
        calls:
            - [addChild, ["@admin.wconf"]] 
        tags:
            - { name: sonata.admin, manager_type: orm, label: "Types de contrats" }
        public: true
 

Trong 2 bộ điều khiển quản trị, bạn sẽ cần thêm tham chiếu đến các thư viện sau:

use Knp\Menu\ItemInterface as MenuItemInterface;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Route\RouteCollection;
use Pix\SortableBehaviorBundle\Services\PositionORMHandler as PositionHandler;

Sau đó trong giao diện Cha, chúng tôi thêm các nút.
Đối với url của giao diện Con, chúng tôi phải cung cấp tham chiếu của dịch vụ để nó tạo ra các tuyến đường. Vì dịch vụ của chúng tôi là “admin.wconf”, tham chiếu cho danh sách sẽ là 'admin.wconf.list'

    protected function configureSideMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null): void
    {
        if (!$childAdmin && !\in_array($action, ['edit'], true)) {
            return;
        }
               
        $admin = $this->isChild() ? $this->getParent() : $this;
        $id = $admin->getRequest()->get('id');
        $label=$this->hasSubject() && null !== $this->getSubject()->getLabel() ? $this->getSubject()->getLabel():null;
        
        $menu->addChild(
            'Configuration du contrat '.$label,
            $admin->generateMenuUrl('edit', ['id' => $id])
           
            );
        
        $menu->addChild( 'Configuration des interfaces '.$label, 
            [
                'uri' => $admin->generateUrl('admin.wconf.list', ['id' => $id]) 
            ]);
        
    }

Giờ đây giao diện CRUD Con của chúng tôi đã bị hạn chế trong phạm vi lựa chọn của Cha, và vì chúng tôi muốn quản lý thứ tự bằng kéo và thả, chúng tôi sẽ loại bỏ khả năng sắp xếp danh sách.
Điều này chỉ đơn giản là thêm đối số sortable=false (mặc định là true), khi định nghĩa các trường danh sách.

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper->add('_action', null, array(
            'actions' => array(
                'move' => array(
                    'template' => '@PixSortableBehavior/Default/_sort_drag_drop.html.twig',
                    'enable_top_bottom_buttons' => false, //optional
                ),
            ),
        ))
        ;
        
        $listMapper->addIdentifier('etape', null, ['label' => 'Etape','sortable'=>false]);
        $listMapper->addIdentifier('position', null, ['label' => 'position','sortable'=>false]);
        //$listMapper->addIdentifier('id', null, ['label' => 'id','sortable'=>false]);
        $listMapper->addIdentifier('label', null, ['label' => 'Label','sortable'=>false]);
        
        
        $listMapper->addIdentifier('synchro_field', null, ['label' => 'Destination du champs','sortable'=>false]);
        $listMapper->add('actif', null, ['editable' => true,'sortable'=>false]);
        
    }

Chúng tôi cũng muốn bảo toàn thứ tự mặc định, để người dùng luôn tìm thấy cùng một thứ tự sau mỗi lần tải lại.
Có hai cách.


Cách đầu tiên là ghi đè cấu hình mặc định:

public function __construct( $code, $class, $baseControllerName ) {
        parent::__construct( $code, $class, $baseControllerName );

}
 protected $datagridValues = array(
        '_page' => 1,
        '_sort_by' => 'position',
        '_sort_order' => 'ASC',
    );

Phương pháp thứ hai cho phép sắp xếp tinh vi hơn, ví dụ trên 2 trường bằng cách ghi đè truy vấn được sử dụng cho danh sách.

    public function createQuery($context = 'list')
    {
        $proxyQuery = parent::createQuery('list');
        $proxyQuery->addOrderBy($proxyQuery->getRootAlias().'.etape', 'ASC');
        $proxyQuery->addOrderBy($proxyQuery->getRootAlias().'.position', 'ASC');
    
        return $proxyQuery;
    }
    

Và chúng tôi nhận ra rằng nếu chúng tôi vô hiệu hóa thứ tự của các cột, thứ tự của chúng tôi cấu hình trong truy vấn của chúng tôi không còn hoạt động.
Vì vậy, điều chúng tôi sẽ làm là ghi đè mẫu cho danh sách này, và loại bỏ mã quản lý tiêu đề cột, nhưng giữ lại mọi thứ khác.

Danh sách được tìm thấy trong vendor/sonata-project/admin-bundle/src/Resources/views/CRUD/base_list.html.twig và khối liên quan của chúng tôi là “table_header”

Sélection_088

Chúng tôi sẽ thêm mẫu của chúng tôi vào khai báo dịch vụ.
Để làm điều đó, chúng tôi phải lấy tham chiếu của mẫu, được mô tả trên trang này: https://symfony.com/doc/master/bundles/SonataAdminBundle/reference/templates.html#global-templates
Trong trường hợp của chúng tôi, nó là

Sélection_089

Và chúng tôi chỉ thêm một lời gọi kiểu setTemplate với cấu hình của chúng tôi

    admin.wconf:
        class: App\Admin\WconfAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, label: "Configuration des types" }
        public: true
        arguments: [~, App\Entity\Wconf, 'PixSortableBehaviorBundle:SortableAdmin']
        calls:
            - [setPositionService, ["@pix_sortable_behavior.position"]]
            - [ setTemplate, [list, "Admin/wconf-list.html.twig"]]

Chúng tôi đặt: “Admin/wconf-list.html.twig”
Điều này có nghĩa là hệ thống sẽ đi tìm tệp /templates/Admin/wconf-list.html.twig
Tệp này phải mở rộng giao diện ban đầu của danh sách, sau đó ghi đè khối quản lý tiêu đề chỉ. Chúng tôi thực sự muốn giữ lại mọi thứ khác.
Vì vậy, chúng tôi bắt đầu tệp của chúng tôi bằng cách cung cấp tham chiếu của mẫu chính, và sau đó chúng tôi ghi đè khối của chúng tôi

#/templates/Admin/wconf-list.html.twig
{% extends '@SonataAdmin/CRUD/base_list.html.twig' %}

        {% block table_header %}
        
                            <thead>
                                <tr class="sonata-ba-list-field-header">
                                    {% for field_description in admin.list.elements %}
                                        {% if admin.hasRoute('batch') and field_description.getOption('code') == '_batch' and batchactions|length > 0 %}
                                            <th class="sonata-ba-list-field-header sonata-ba-list-field-header-batch">
                                              <input type="checkbox" id="list_batch_checkbox">
                                            </th>
                                        {% elseif field_description.getOption('code') == '_select' %}
                                            <th class="sonata-ba-list-field-header sonata-ba-list-field-header-select"></th>
                                        {% elseif field_description.name == '_action' and app.request.isXmlHttpRequest %}
                                            {# Action buttons disabled in ajax view! #}
                                        {% elseif field_description.getOption('ajax_hidden') == true and app.request.isXmlHttpRequest %}
                                            {# Disable fields with 'ajax_hidden' option set to true #}
                                        {% else %}
                                            {% set sortable = false %}
                                            {% apply spaceless %}
                                                <th class="sonata-ba-list-field-header-{{ field_description.type }}{% if sortable %} sonata-ba-list-field-header-order-{{ sort_by|lower }} {{ sort_active_class }}{% endif %}{% if field_description.options.header_class is defined %} {{ field_description.options.header_class }}{% endif %}"{% if field_description.options.header_style is defined %} style="{{ field_description.options.header_style }}"{% endif %}>
                                                    {% if field_description.getOption('label_icon') %}
                                                        <i class="sonata-ba-list-field-header-label-icon {{ field_description.getOption('label_icon') }}" aria-hidden="true"></i>
                                                    {% endif %}
                                                    {{ field_description.label|trans({}, field_description.translationDomain) }}
                                                   
                                                </th>
                                            {% endapply %}
                                        {% endif %}
                                    {% endfor %}
                                </tr>
                            </thead>
            {% endblock %}

Sau đó chúng ta có tiêu đề của mình mà không có các liên kết để sắp xếp nội dung.
Chúng ta đảm bảo rằng người dùng sẽ không quản lý thứ tự bằng cách kéo và thả với một cấu hình sắp xếp kém và gọi cho chúng ta mỗi 5 phút để nói rằng nó không hoạt động.