Sonata Admin Bundle: Editable suggest field in list view - symfony

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 :)

Related

Symfony2 Multiple form Buttons

I have an Order Entity.
I have a Status Entity that contains a number of status options, ie. Submitted, Invoiced, Paid, Shipped (id 1-4). I have a page that I want to list a number of buttons on it each corresponding to a certain status as mentioned above. Basically the reason for the multiple buttons is that I want the user to be able to basically skip certain status records by just clicking the one they want... I have some logic that will determine what buttons to present however my problem is being able to tell what button was clicked when the form is submitted.
From the docs the only way I know to see if a button was clicked is to use:
$form->get('buttonName')->isClicked();
The way i am thinking about this right now is that my buttons will be named by the statusId that it corresponds to. ie: $form->add('status1', SubmitType::class) # being the ID of the status record.
this problem with this is that the buttons generated on the page will be dynamic. So it would seem like at this point I would need to query my DB for all the statuses and iterate through all the possibilities:
foreach ($statuses as $status)
{
if($form->get('status' . $status->getId())->isClicked()){
{
//Do Something
break;
}
}
I feel like there has to be a better way... IE: $form->getSubmitButtonName() which would return "status#" then I could just do a substr() to get the ID out of the name..
I would suggest using a dropdown menu instead of 4 buttons, and just a simple submit button to save it. It should fix your problem, and look a lot better then having 4 identical buttons that just save different values, example code below:
The Entity should look something like this: (us the toString method to display the actual names instead of (0, 1, 2, 3)
abstract class OrderStatus{
const submitted = 0;
const Ivoiced = 1;
const Paid = 2;
const Shipped = 3;
}
/**
* #ORM\Column(type="integer")
*/
protected $order_status; // see OrderStatus enum above
/**
* Set orderStatus
*
* #param integer $orderStatus
*
* #return Order
*/
public function setOrderStatus($orderStatus)
{
$this->order_status = $orderStatus;
return $this;
}
/**
* Get orderStatus
*
* #return integer
*/
public function getOrderStatus()
{
return $this->order_status;
}
public function getOrderStatusToString()
{
switch ($this->getOrderStatus()) {
case 0: return "submitted"; break;
case 1: return "invoiced"; break;
case 2: return "paid"; break;
case 3: return "Shipped"; break;
}
}
add this in the controller:
//the dropdown menu made in form (can be put in a formType instead of controller if you wish)
$form = $this->createFormBuilder(your.entity.name)
->add('orderStatus', 'choice', array(
'choices' => array(
'submitted' => 0,
'invoiced' => 1,
'paid' => 2,
'Shipped' => 3,
),
'choices_as_values' => true,
'label'=>false
))
$form->add('save', 'submit', array('label' => 'Save'));
$form = $form->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em=$this->getDoctrine()->getManager();
$em->persist(your.entity.name);
$em->flush();
}
in your html file smth like: (with any css you want)
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
This all together (with some tweaks) should do the trick

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

How to add a global action in Sonata Admin?

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;
}

How to properly set referring entity property when creating sub entity in Sonata Admin Bundle using sonata_type_model field

I feel like I'm missing something simple here...TLDR: using sonata_type_model field, which uses modal when adding new sub entities to a parent, how do I pass the parent to the sub entity to add it to the sub entity's reference field?
#
I have two entities at play, "User" and "Role" entities.
User -> OneToMany -> Role.
I'm trying to figure out how to create, edit, and delete roles from a user's Sonata Admin Bundle page.
In my UserAdmin class, I've configured the form fields like so:
$formmapper->add('roles', 'sonata_type_model', array(
'required' => false,
'btn_delete' => true,
'btn_list' => true,
'multiple' => true,
'btn_add' => true,
))
Existing roles show up fine. If I click the "add" button under the field of roles the Modal window appears with the fields from my "role" admin form. My problem is that when I save the new Role it does not properly reference the User on which it was created. I don't know how to pass the parent USER entity to the child ROLE entity! This should be simple but. I cannot find this answer anywhere
If you want one User can have many roles ('multiple => true' option in your class), and role can be used by many users, you should prefer a ManyToMany relationship.
Use something like this for replace your actual OneToMany:
//Entity\User
/**
* #ORM\ManyToMany(targetEntity="Role", mappedBy="users", cascade={"persist"})
*/
protected $roles;
And in your child entity :
//Entity\Role
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="roles", cascade={"persist", "remove"})
* #ORM\JoinTable(name="users_roles",
* joinColumns={#ORM\JoinColumn(name="role_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")}
* ) *
* #var ArrayCollection $users
*/
protected $users;
Your parent entity (User) should have methods addRole(), removeRole() and getRole().
Else, you should do php app/console doctrine:generate:entities
If it's good, you have to edit your addRole() and removeRole() methods.
She must be like this :
public function addRole(\Namespace\Bundle\Entity\Role $roles)
{
$this->roles[] = $roles;
$roles->addUser($this);
return $this;
}
public function removeRole(\Namespace\Bundle\Entity\Role $roles)
{
$this->roles->removeElement($roles);
$roles->removeUser($this);
}
Then, your addRole should work in Sonata
I don't think this is the intended way to solve this problem, but needing to set the "user" entity on the "role" entity form that opens in an modal window when editing the User entity was achieved by using the jQuery ajaxComplete() function to listen for ajax windows opening, checking to see if it was the right one, grabbing the User ID from the page URL, and setting that ID in the hidden form element
jQuery(document).ready(function() {
$(document).ajaxComplete(function (event, request, settings) {
if (settings.url.indexOf('/your-entity-admin-path-') >= 0){
var pathArray = window.location.pathname.split( '/' );
$('.modal-body .box-body input[type=hidden]').val(pathArray[3]);
}
});
});
A nasty solution but sometimes we just need things to work...

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