How to add a global action in Sonata Admin? - symfony

Well, I have a fairly basic problem with Sonata Admin in my Symfony2 project.
I have a "products" list view with every product sold on my web store. On the top right "actions" menu, I have the default actions, with a single action named "Add new".
I just want to add more actions next to "Add new": custom actions like "remove promo prices from all products", or "remove all products evaluations".
I don't want a "batch" action, I want a "global" action leading to a custom DB query.
All I find in the doc is related to batch actions or "single line action". Is there a way to do what I want ?
Thank you for your help !

Create and configure a custom admin extension and override the configureActionButtons(AdminInterface $admin, $list, $action, $object) method to add custom actions:
use Sonata\AdminBundle\Admin\AdminExtension;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Route\RouteCollection;
class CustomGlobalActionsExtension extends AdminExtension
{
public function configureActionButtons(AdminInterface $admin, $list, $action, $object)
{
return array_merge($list, [
['template' => 'admin/custom_action.html.twig']
]);
}
public function configureRoutes(AdminInterface $admin, RouteCollection $collection)
{
$collection->add('custom_action', $admin->getRouterIdParameter().'/custom_action');
}
}
{# app/Resources/views/admin/custom_action.html.twig #}
<a class="btn btn-sm" href="{{ admin.generateObjectUrl('custom_action', object) }}">Custom Action</a>
See also https://sonata-project.org/bundles/admin/2-3/doc/cookbook/recipe_custom_action.html

Is syntax changed a little bit and it is important to call the parent method:
/**
* #param $action
* #param null|object $object
* #return array
*/
public function configureActionButtons($action, $object = null)
{
$buttonList = parent::configureActionButtons($action, $object);
$buttonList['create_custom'] = [
'template' => 'admin/action/button.html.twig'
];
unset($buttonList['not_wanted']);
return $buttonList;
}

Related

Drupal 8 - How do I detect whether any contact forms have been created?

Context: On my Drupal 8 site I'm trying to add in a new section containing a view that lists out all forms. The user can then click on one of the form and it launches a download of all the messages sent through that form. However that section should only be visible if the user has created at least 1 form.
What I've tried: So far what I've discovered is that the contact forms are stored in the config table in the database, so I could theoretically run a query like "SELECT 1 FROM config WHERE name LIKE '%contact.form%'" or whatever the equivalent in D8 is. However, it seems like there has to be a quicker way to return either the presence of contact forms or the contact forms themselves programmatically.
Final question: How can I return either the contact forms themselves or at least a boolean representing the presence of contact forms programmatically?
Querying contact form entities
\Drupal::entityTypeManager()
->getStorage('contact_form')
->loadMultiple();
=> [
"feedback" => Drupal\contact\Entity\ContactForm {#3213},
"personal" => Drupal\contact\Entity\ContactForm {#3994},
]
Contact forms count
\Drupal::entityTypeManager()
->getStorage('contact_form')
->getQuery()
->count()
->execute();
=> 2
And on your final question; 1 way to approach this, is to subclass ContactFormListBuilder and override its default render() logic:
Routing YAML
my_module.custom_contact_form_list:
path: '/admin/foo/custom-contact-form-listing'
defaults:
_controller: '\Drupal\my_module\Controller\ContactFormListController::listing'
_title: 'A custom contact form listing view'
requirements:
_permission: 'administer contact forms'
Listing Controller
<?php
namespace Drupal\my_module\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\my_module\CustomContactFormListBuilder;
/**
* Class ContactFormListController.
*
* #package Drupal\my_module\Controller
*/
class ContactFormListController extends ControllerBase {
/**
* Provides a custom listing page for contact forms.
*
* #return array
* A render array as expected by
* \Drupal\Core\Render\RendererInterface::render().
*/
public function listing() {
$definition = $this->entityTypeManager()->getDefinition('contact_form');
return $this
->entityTypeManager()
->createHandlerInstance(CustomContactFormListBuilder::class, $definition)
->render();
}
}
List Builder class
<?php
namespace Drupal\my_module;
use Drupal\contact\ContactFormListBuilder;
/**
* Class CustomContactFormListBuilder.
*
* #package Drupal\my_module
*/
class CustomContactFormListBuilder extends ContactFormListBuilder {
/**
* {#inheritdoc}
*/
public function render() {
return parent::render(); // TODO: Change the autogenerated stub
}
}

Sonata Admin Bundle: Editable suggest field in list view

In the list field you can make a field editable by setting the attribute "editable" to "true" in the configureListFields action. Is it possible (with onboard sonata admin tools) to make a field editable that contains multiple values as in a one-to-many relation?
Example:
I have a list of pupils listed in the list view. Every pupil has multiple classes listet in the classes column of the pupils list view. Via click on the classes I want a popover open (like it works with a normale string) with a suggest field like you can have it in the edit view.
Using the properties like in the configFormFields action doesn't work:
$listMapper->add(
'classes',null, array(
'editable' => true,
'type' => 'sonata_type_model_autocomplete',
'multiple' => true,
'property' => 'name'
)
);
That snippet is written inside the PupilsAdmin class in the configureListFields action.
Is it possible or do I have to create a custom template?
The documentation doesn't point me in the right direction: https://sonata-project.org/bundles/admin/2-2/doc/reference/field_types.html
If i understand you right, you want to edit a one-to-many relation inline in a list view of sonata. As far as i know, thats only possible for simple types like text, int or choices and so on. They point it out on no 18. in your link
Theses types accept an editable parameter to edit the value from within the list action. This is currently limited to scalar types (text, integer, url...).
So related objects can not be in that list, merely their scalar properties. For all other things you have to write your own template ...
I don't know what you want to achieve with this suggested list, but for me it makes no sense to edit a one-to-many property in the list view like its done in the edit view.
You just need to create new type. Something like "entity"
'header_class' => 'col-lg-1',
'class' => Employee::class,
'editable' => true,
])
Next step is to override fixFieldDescription method in listBuilder and handle it
class EntityListBuilder extends ListBuilder
{
/**
* #var Registry
*/
private $doctrine;
/**
* #param AdminInterface $admin
* #param FieldDescriptionInterface $fieldDescription
*/
public function fixFieldDescription(AdminInterface $admin, FieldDescriptionInterface $fieldDescription)
{
parent::fixFieldDescription($admin, $fieldDescription);
if ($fieldDescription->getType() === 'entity') {
$class = $fieldDescription->getOption('class');
if (!$class) {
throw new RuntimeException("Type entity must contain 'class' argument");
}
$objects = $this->doctrine->getRepository($class)->findAll();
$choices = [];
foreach ($objects as $object) {
$choices[$object->getId()] = $object->__toString();
}
$fieldDescription->setOption('choices', $choices);
$fieldDescription->setType('choice');
}
}
/**
* #param Registry $doctrine
*/
public function setDoctrine(Registry $doctrine)
{
$this->doctrine = $doctrine;
}
/**
* #param string $type
*
* #return string
*/
private function getTemplate($type)
{
return $this->templates[$type] ?? '';
}
Now, you have to override template for your "entity" type
{% extends '#SonataAdmin/CRUD/list_choice.html.twig' %}
{% set value = admin.id(value) %}
It's need to set already chosen value to select box
Okay, last thing is to add our type to xeditable types of Twig
Add it to OverrideServiceCompilerPass :
$definition = $container->getParameter('sonata.admin.twig.extension.x_editable_type_mapping');
$definition['entity'] = 'select';
$container->setParameter('sonata.admin.twig.extension.x_editable_type_mapping', $definition);
And the last one just match your type with template
templates:
types:
list:
...
entity: AppBundle:CRUD:list_entity.html.twig
Now you ready to edit it inline :)

Sonata Admin Bundle loosing filter settings on edit

I created a standard simple admin for some entities, according to the Sonata handbook.
The problem is that the configured filter gets lost when editing an enity. Say I have set 3 filter values and then click on an entity to edit it. Neither "Save" nor the action "Back to list" brings me back to the filtered list. Even pagination starts from 1 again.
How can I keep the set filter?
This is an example admin class:
namespace AppBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
/**
* Description of OrtAdmin
*
* #author markus
*/
class OrtAdmin extends AbstractAdmin{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('name', 'text');
}
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add('name');
}
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('name', 'string');
}
//Remove some export formats
public function getExportFormats() {
return array(
'csv', 'xls'
);
}
//No batch actions
public function getBatchActions() {
$actions = parent::getBatchActions();
unset($actions['delete']);
return $actions;
}
}
Your filter gets lost after leaving the list view. Re-Opening the list (without getting back to the same URL) will always result in your pre-configured filters, which you can define on per-Admin-class.
Simply enable persistent filters per configuration. Please be aware, that those get persisted into your user's session, which mean they will only reset or change if you press the button "reset filters".
You can easily activate the option like this:
sonata_admin:
persist_filters: true
There is no dedicated documentation, but you can find the option in SonataAdmin Full Configuration Options.

How to create a form using block module in drupal 8?

I want build a form using a block module in Drupal 8. I am aware of building the forms in Drupal 7 but the same seems to be different in Drupal 8.
Request anyone who has worked on drupal8 custom forms as block to help me.
Your question is very vague, as I don't know how much you already know about modules, forms and blocks in Drupal 8. So here is a small guide what to do, further information on how to do stuff in detail would be overkill for this answer.
1. Create a new module and enable it
Look here: Naming and placing your Drupal 8 module.
Basically you create the module folder and the module info yml file to let Drupal know about the module. Then you enable it using drush or the admin area in Drupal.
2. Create the form
Look here: Introduction to Form API.
under your_module/src/Form you create the form. More details in the link above.
3. Create the block and render the form
Look here: Create a custom block.
under your_module/src/Plugin/Block/ you create the block which will render the form.
The idea is basically (code updated with suggestion from Henrik):
$builtForm = \Drupal::formBuilder()->getForm('Drupal\your_module\Form\Your‌​Form');
$renderArray['form'] = $builtForm;
return $renderArray;
Note: You don't need to wrap the $builtForm with the $renderArray, you can return just the $builtForm and be fine. I just personally like to do it that way, because often times I need to add something else to the final render array like some markup, cache settings or a library etc.
4. Place the block
Place the block in the desired region(s). Done.
To build a form using block module, you can easily use Webform module where you can add a form and display as a block.
If you mean to create a form programatically in the custom block, you can achieve that by creating two files shown below:
Form file (src/Form/DemoForm.php):
<?php
/**
* #file
* Contains \Drupal\demo\Form\DemoForm.
*/
namespace Drupal\demo\Form;
use Drupal\Core\Form\FormBase;
class DemoForm extends FormBase {
/**
* {#inheritdoc}.
*/
public function getFormId() {
return 'demo_form';
}
/**
* {#inheritdoc}.
*/
public function buildForm(array $form, array &$form_state) {
$form['email'] = array(
'#type' => 'email',
'#title' => $this->t('Your .com email address.')
);
$form['show'] = array(
'#type' => 'submit',
'#value' => $this->t('Submit'),
);
return $form;
}
/**
* {#inheritdoc}
*/
public function validateForm(array &$form, array &$form_state) {
$values = $form_state->getValues();
if (strpos($values['email'], '.com') === FALSE ) {
$form_state->setErrorByName('email', t('This is not a .com email address.'));
}
}
/**
* {#inheritdoc}
*/
public function submitForm(array &$form, array &$form_state) {
drupal_set_message($this->t('Your email address is #email', array('#email' => $form_state['values']['email'])));
}
}
Source: Building a Drupal 8 Module: Blocks and Forms.
Block file (src/Plugin/Block/HelloBlock.php):
<?php
namespace Drupal\mymodule\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a 'Hello' Block.
*
* #Block(
* id = "form_block",
* admin_label = #Translation("My form"),
* category = #Translation("My Category"),
* )
*/
class HelloBlock extends BlockBase {
/**
* {#inheritdoc}
*/
public function build() {
$form = \Drupal::formBuilder()->getForm('\Drupal\mymodule\Form\HelloBlock');
//$form['#attached']['js'][] = drupal_get_path('module', 'example') . '/js/example.js';
//$form['#markup'] = $this->t('Custom text');
return $form;
}
}
Source: Create a custom block.
To add a form to the Block Configuration, see: Add a Form to the Block Configuration.
Here is a detailed summary of how to go about this:-
https://www.sitepoint.com/building-drupal-8-module-blocks-forms/
Following the above guide, you would add the completed form to the block build function, e.g.
class DemoBlock extends BlockBase {
/**
* {#inheritdoc}
*/
public function build() {
$form = \Drupal::formBuilder()->getForm('Drupal\demo\Form\DemoForm');
return $form;
}
}
Some more useful docs if you are new to Drupal 8 or need to dust off your knowledge:
https://www.drupal.org/docs/8/creating-custom-modules
https://www.drupal.org/docs/8/api/block-api
https://www.drupal.org/docs/8/api/form-api

Sonata admin - "order by" field in related table

I have a Product admin class. The Product entity has a many-to-one relationship with a Category entity, i.e. a product is associated with a category.
In the admin "list" page for products, I need to sort by the category name (alphabetically) that each product is associated with.
Setting the default field to sort by is easy if the field is on the entity itself (see Sonata admin bundle order for how to do this). But I cannot figure out how to sort by a field in a related table.
Any help is appreciated.
It seems a workaround, but it works. You have to add a join overriding createQuery() method, than assign a default sortBy overriding $datagridValues:
<?php
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery;
class ExpenseAdmin extends Admin
{
protected $datagridValues = array(
'_page' => 1,
'_sort_order' => 'ASC', // sort direction
'_sort_by' => 'c.name' // field name
);
/**
* #return \Sonata\AdminBundle\Datagrid\ProxyQueryInterface
*/
public function createQuery($context = 'list')
{
$query = parent::createQuery($context);
return new ProxyQuery($query
->join(sprintf('%s.category', $query->getRootAlias()), 'c'));
}
}
Asume name is the property of entity Category by wich you want to sort. You may do this in you ProductAdmin.php
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->add('category.name', null, array(
'sortable' => true,
));
...
}
This way you leverage the ordering links in the header of the list, generated by Sonata.
Edit
If you would also like to have a link on the category name in products list to quickly edit the Category entity, assuming you have created a CategoryAdmin class, you should write your code like this:
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->add('category', null, array(
'sortable' => 'category.name',
));
...
}
And in your Category class you should implement the __toString() method like this:
public function __toString()
{
return $this->getName();
}

Resources