Skip to content

Commit

Permalink
Add sortable admin list feature
Browse files Browse the repository at this point in the history
  • Loading branch information
bdejacobet committed Mar 16, 2018
1 parent 13cba06 commit 35fb8ba
Show file tree
Hide file tree
Showing 12 changed files with 698 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- Add new form type : ImmutableTabsType
- Add new feature : sortable admin list
5 changes: 5 additions & 0 deletions docs/00-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
## Immutable tabs form type

- [Immutable Tabs Form Type](./10-immutable-tabs-type.md)


## Sortabe admin list

- [Sortabe admin list](./20-Sortable-admin-list.md)
272 changes: 272 additions & 0 deletions docs/20-Sortable-admin-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
# Admin sortable list

To sort an admin list by drag&drop

![sortable-admin-list.png](img/sortable-admin-list.png)

## How it works

Specific templates are used by your admin to add a class to HTML table to init javascript drag&drop:
- sonata/src/Ressources/views/CRUD/list_outer_rows_sortable_list.html.twig
- sonata/src/Ressources/views/CRUD/list_sortable.html.twig

When you drop the row of your list, an ajax call send all positions of your items list to a new route of your admin to save all positions

## Installation

You must install some javascript libraries to your project :
- "@shopify/draggable": "^1.0.0-beta.4"
- "superagent": "3.8.2"

You must include css sonata/src/Ressources/public/css/sortableAdminList.css to your project

You must call the sonata/src/Ressources/public/js/backoffice.js to init drag&drop if a sortable list is display

You must declare a new service
```
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!--SortAction-->
<service id="admin.sort_action" class="SortAction\SortAction">
<argument type="service" id="doctrine" />
</service>
</services>
</container>
```

## How to implement drag&drop to your admin

To use drag&drop to your entity admin, you must update your code like below

### Position field

You must add a field position to your entities
(you can set an other name for this field if you want)

```
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="CanalPlus\Bundle\AppBundle\Entity\YourEntity" table="your_entity_table">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="position" type="integer" column="position" nullable="true"/>
...
</entity>
</doctrine-mapping>
```


```
class YourEntity
{
...
/**
* @var int
*/
protected $position;
...
/**
* {@inheritdoc}
*/
public function setPosition($position)
{
$this->position = $position;
}
/**
* {@inheritdoc}
*/
public function getPosition()
{
return $this->position;
}
}
```

### Admin

You must add some config to your admin object :
- list is the only mode, you must disable mosaic button
- your list must be only sorted by position asc
- you must configure pagination to display all objects in a single page
- you must add a new route `save_positions`
- you must configure admin to use specific template twig
- a new object will be saved with a position


```
<!-- admin.xml -->
<service id="admin.your_entity" class="YourEntityAdminPath">
<tag name="sonata.admin" manager_type="orm" label="Your entity admin" show_mosaic_button="false"/> <!-- disable mosaic button -->
<argument />
<argument>YourEntityPath</argument>
<argument>YourEntityAdminControllerPath</argument> <!-- specific controller -->
<call method="setRegistry"> <!-- to get the last position to create new entity with position -->
<argument type="service" id="doctrine"/>
</call>
</service>
```


```
class YourEntityAdmin extends AbstractAdmin
{
// Sortable list can be only with one page
protected $maxPerPage = 500;
protected $perPageOptions = [500];
protected $datagridValues = [
'_page' => 1,
'_sort_order' => 'ASC',
'_sort_by' => 'position',
];
...
/**
* {@inheritdoc}
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('name', TextType::class, [
'label' => 'Nom',
'sortable' => false, // Sorted only by position asc
])
->add('_action', null, [
'actions' => [
'edit' => [],
'delete' => [],
],
'row_align' => 'center',
'header_style' => 'width:20%',
])
;
}
/**
* {@inheritdoc}
*/
protected function configureRoutes(RouteCollection $collection)
{
parent::configureRoutes($collection);
$collection->add('save_positions'); // New route to save positions
}
/**
* {@inheritdoc}
*/
public function getTemplate($name)
{
switch ($name) {
case 'list': // Templace with css classes to get drag&drop
return 'CanalPlusAwakenAdminBundle:CRUD:list_sortable.html.twig';
case 'outer_sortable_list_rows_list': // Templace with css classes to get drag&drop
return 'CanalPlusAwakenAdminBundle:CRUD:list_outer_rows_sortable_list.html.twig';
default:
return parent::getTemplate($name);
}
}
/**
* {@inheritdoc}
*/
public function prePersist($object)
{
parent::prePersist($object);
// Set position
$connection = $this->doctrine->getConnection();
$stmt = $connection->executeQuery(
'SELECT MAX(position) as position FROM your_entity_table;',
);
$result = $stmt->fetch();
$object->setPosition($result['position'] + 1);
return [];
}
}
```

### Admin controller

A new action `savePositionsAction` for the route `save_positions` that will save new positions of all objects
This action use the service `admin.sort_action` that will update field `position` in database
```
class YourEntityAdminController extends Controller
{
/**
* @param Request|null $request
*
* @return Response
*/
public function savePositionsAction(Request $request = null)
{
$this->admin->checkAccess('list');
$idsByPositions = $request->request->get('idsByPositions');
if (!$idsByPositions) {
return $this->renderJson(['status' => 'error', 'message' => 'idsByPositions is null'], 500);
}
// Update positions
$idsByPositions = json_decode($idsByPositions, true);
$sortAction = $this->get('admin.sort_action');
$result = $sortAction->sortEntities(DecoderChannelsGroup::class, $idsByPositions);
if ($result['status'] === 'error') {
return $this->renderJson($result, 500);
}
return $this->renderJson($result);
}
```

### sort_action service

`/sonata/src/SortAction/SortAction.php`

This service generate and execute a query to update all positions by id
```
UPDATE decoder_channels_group SET position = (
case when id = 1 then 0
when id = 2 then 1
when id = 3 then 2
end
) WHERE id in ( 1, 2, 3)
```

You can add a new sortable class to define it database table and it sort field
```
class SortAction
{
// Sortable class names and their database table name and sort field
const SORTABLE_CLASS = [
YourEntity::class => [
'table' => 'your_entity_table',
'field' => 'position',
],
];
...
```
Binary file added docs/img/sortable-admin-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions src/Ressources/public/css/sortableAdminList.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* sortable table element*/
.sortable-table tr {
cursor: move;
}

/* draggable elements */
.draggable--over {
opacity: 0.33;
}
.draggable-mirror {
border: solid 1px #3c8dbc;
}
28 changes: 28 additions & 0 deletions src/Ressources/public/js/Sortable/Sortable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* This file is part of the Sonata for Ekino project.
*
* (c) 2018 - Ekino
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/

import SortableTable from './SortableTable';

/**
* Initialize SortableList behavior on all suitable children of the given node
*
* @param {Element} node
*/
export default function initializeSortableList (node) {
if (!node.querySelectorAll) {
return;
}

const listNodes = node.querySelectorAll('.sonata-ba-sortable-list');
listNodes.forEach((listNode) => {
const sortableTable = new SortableTable(listNode);
sortableTable.initialize();
});
};
Loading

0 comments on commit 35fb8ba

Please sign in to comment.