EasyCorp / EasyAdminBundle

EasyAdmin is a fast, beautiful and modern admin generator for Symfony applications.
MIT License
4.09k stars 1.03k forks source link

Add support for custom actions #194

Closed javiereguiluz closed 9 years ago

javiereguiluz commented 9 years ago

We've talked a lot about custom actions and there are already some PR to add them (#119 and #137). So it's time to add custom actions in EasyAdmin.

Before writing a single line of code, let's agree on what we want to do and how are going to do it. This is what I propose:

1) New custom_actions option

I prefer not to use the existing actions option because that would mean to redefine all entity actions whenever you add a custom action. Example:

User:
    class: AppBundle/Entity/User
    actions: ['myAction']

The User entity has just lost all the default actions: edit, show, delete, etc. To avoid losing these actions, you have to write them whenever you use a custom action:

User:
    class: AppBundle/Entity/User
    actions: ['myAction', 'edit', 'new', 'search', ...]

If Symfony YAML component supported references, we could do the following trick:

easy_admin:
    actions: &default_actions
        - 'edit'
        - 'new'
        - 'show'
    entities:
        User:
            class: AppBundle/Entity/User
            actions:
                <<: *default-actions
                - myAction

But this is not supported, so I propose to define a new custom_actions, which solves all problems in a simple and pragmatic way:

User:
    class: AppBundle/Entity/User
    actions: ['edit', 'new', 'search']
    custom_actions: ['myAction']

Product:
    class: AppBundle/Entity/Product
    actions: ['show', 'search']

Invoice:
    class: AppBundle/Entity/Product
    custom_actions: ['resend', 'setAsPAid']

This is an entity-level option. You cannot define a custom_actions global option to apply it to all entities and you cannot specify custom actions per entity page (list, edit, etc.) All custom actions will be displayed in the list action of the entity.

2) How do custom actions work

Common case: define a method called as the custom action

User:
    class: AppBundle/Entity/User
    custom_actions: ['grantAccess']

EasyAdmin executes the grantAccess() method of the AdminController and it will pass as argument the id of the active item. Obviously you have to define your own AdminController extending from the default one.

Extended configuration for the common case: define a label for the action

User:
    class: AppBundle/Entity/User
    custom_actions: [ { action: 'grantAccess', label: 'Grant' } ]

Custom case: define the name of the route which will respond to the action

User:
    class: AppBundle/Entity/User
    custom_actions: [ { route: 'user_grant_access', label: 'Grant' } ]

In this case, a link will be generated with the path() Twig function using that route name and passing the id of the active item as the only route parameter.

ogizanagi commented 9 years ago

1) New custom_actions option

I presume you're not in favor of some kind of magical values in order to inherit global configured actions and get rid of the custom_action necessity ?

easy_admin:
    actions: ['edit', 'new', 'show']
    entities:
        User:
            class: AppBundle/Entity/User
            actions:
                - inherit # will inherit from easy_admin.action
                - ... #same
                - '<<' #same
                - myAction

User.actions will be merged with easy_admin.actions during normalization.

But then, there are many other concerns: If we do that, should built-in actions like list be overridable with a custom definition :

User:
    class: AppBundle/Entity/User
    actions: [ { route: 'user_list', action: 'list' } ]

What happens here ?

2) How do custom actions work

What if a custom action isn't dedicated to a single record, but the whole entity ? As a statistics panel ? Are custom actions only about performing something against one entity record ?

In this case, a link will be generated with the path() Twig function using that route name and passing the id of the active item as the only route parameter.

Could custom actions be registered globally ? If so, shouldn't we generate the link with the entity name + id as route parameters ? NVM, i forgot about this part:

This is an entity-level option. You cannot define a custom_actions global option to apply it to all entities and you cannot specify custom actions per entity page (list, edit, etc.) All custom actions will be displayed in the list action of the entity.

But why not ?

javiereguiluz commented 9 years ago

@ogizanagi thanks for your comments. Answering your concerns:

sr972 commented 9 years ago

EasyAdmin executes the grantAccess() method of the AdminController and it will pass as argument the id of the active item. Obviously you have to define your own AdminController extending from the default one.

A little throw-in: Couldn't we use the custom entity repositories instead for this? So there is no need for writing your own AdminController that extends.

EDIT: BTW, i'm for the custom_actions in the yaml configuration too. And i think, it would be nice, to set defaults like you did before in your examples for default. And as long as there is no actions key in the entity section, this will be used. But if there is an actions key in the entity session it will override the default and just use the given actions. Plus, if set, the custom actions in custom_actions

javiereguiluz commented 9 years ago

I'm not sure about adding the option of defining entity repository methods, but let's see the opinions of others. I show bellow some examples of the three proposed types of custom actions.


User:
    class: AppBundle/Entity/User
    custom_actions:
        - { method: 'grantAccess', label: 'Grant' }               # 1
        - { repository: 'softDelete', label: 'Delete' }           # 2
        - { route: 'update_user_information', label: 'Refresh' }  # 3

1) A method of the AdminController (you have to extend the default one and create your own admin controller):

// EasyAdmin executes the following:
$this->{$method}();

// EXAMPLE:
$this->grantAccess();

// ...

public function grantAccess()
{
    $entity = $this->em->find($this->request->query->get('id'));
}

2) A method of the repository related to the current entity:

// EasyAdmin executes the following:
$repository = $this->em->getRepository($this->entity['class'])
$repository->{$repository}($this->request->query->get('id')); 

// EXAMPLE:
$repository = $this->em->getRepository('AppBundle\Entity\User');
$repository->softDelete(12);

3) A Symfony application route:

// EasyAdmin executes the following (in the 'list.html.twig' template):
<a href="{{ path(route, { entity: entity.name, id: entity.id }) }}">
    {{ label }}
</a>

// EXAMPLE:
/**
 * @Route('update_user_information')
 */
public function updateUserInformationAction(Request $request)
{
    $user = $this->get('user_repository')->find($request->query->get('id'));

    // ...

    return $this->redirectToRoute('admin', array(
        'action' => 'list',
        'entity' => $request->query->get('entity'),
    ));
}
Pierstoval commented 9 years ago

First, sorry for the huge paved text :smile:

I'd prefer to keep the use of the actions parameter, and instead of adding a whole custom_actions, using the global actions parameter as reference, and if it's not set, use the six default actions as reference, and add a override_default_actions boolean parameter that would be false by default.

Global scale:

If override_default_actions is true, then only the specified actions are allowed, PLUS the "list" action (which is mandatory). If it's set to false, any action specified in the actions parameter will be merged with the six default actions.

Per-entity scale:

If override_default_actions is true, the actions will finally contain only specified actions (per-entity), and if it's false, the actions will be merged with the back-end global actions (global + per-entity).

This would lead to something like this:

easy_admin:
    actions: [ 'show', 'statistics' ]
    # With this param set to "true", we'll never see the "edit" or "new"
    #  default actions, nor the "toggle" one.
    override_default_actions: true 
    entities:
        Product:
            class: AppBundle\Entity\Products
            # Here we force the override because we don't want "statistics"
            #  for products, but we have a "stocks" action dedicated to this entity
            actions: [ 'show', 'stocks' ]
            override_default_actions: true
        User:
            class: AppBundle\Entity\User
            # Here, there is no "override_default_actions" attribute,
            #  so we'll see all global actions ("show" and "statistics")
            #  PLUS the "edit" action
            actions: [ 'edit' ] 

The very good point of this is that with a default false override_default_actions attribute, there is no fundamental BC break, and any actual back-end can work the same way.

Default actions... Are also "custom" actions!

One another bonus is that the default back-end actions can be treated as "custom" actions with the default parameters already set in the AdminController. And any other action can have its parameters overriden with the route and label parameter. And with this, you can ALSO change the label of the current actions, like this:

easy_admin:
    entities:
        User:
            class: AppBundle\Entity\User
            actions: 
                - { name: 'edit', label: "Modifier les paramètres" } 

(I'm writing while @javiereguiluz just posted his point of view with his 3 methods :smile: )

The future: action "types"

One another lack I see here is that with this system, we may need to specify which type of action we have.

What is an action type? Well... Remember CRUD! In fact, most of the custom actions can:

We always rely on CRUD in a back-end, and sometimes we want to do things that are NOT entity-related (mostly for search and dashboards). So, using a entity_related parameter in our action details might be one solution for this, because all non-entity-related actions can be showed elsewhere in the bundle (for example in the left navbar) And for the rest, the six default actions already rely on the CRUD, so we just might change the default array('edit', 'new', 'list', 'show', 'toggle', 'delete') with a much more complex array BUT with the use of our CRUD system.

This will allow any user to create a dasboard with a defaulted "READ" CRUD parameter, and (it's my case in my actual back-ends) create a specific EDIT crud action to manage entities in a much more different way.

I can give you my example if you need it to see:

One of my apps is a maps manager, but always custom maps (example here with a tabletop rpg map). BUT, managing a map is made in 2 ways: managing the map parameters (name, slug, reference image, zoom level, description...), and managing the map datas: markers, routes and zones. And for this use, I absolutely needed a custom interactive_edit action, which would be a page that shows the same map as the link above, but with all the tools to dynamically and interactively add and position markers, zones and routes, and edit their parameters live onto the map. This action would be of "UPDATE" crud type. And if I don't specify a controller action, it would then use the editAction() with some more parameters given in the config, for example the repository action used (in my case it would be getMapWithFullDatas($id)) , and a simple template attribute (in which I'll add edit_interactive.html.twig, or it can also be defaulted) and voilà, you just have a custom action setup with no controller overridance, just a single template created in app/Resources/views/edit_interactive.html.twig or app/Resources/views/EasyAdminBundle/edit_interactive.html.twig.

Are they good ideas? :)

Pierstoval commented 9 years ago

Ah, and if you'd like to know, I'd be very happy to work on this feature if it corresponds to my vision of it :laughing:

javiereguiluz commented 9 years ago

@Pierstoval thanks for your ideas. Let's go for another round of comments.


I don't like the override_default_actions option because to me it looks like a hack (although it's not). What about implementing the following behavior:

Example:

easy_admin:
    # these 3 actions are the only ones enabled by default
    actions: ['show', 'search', 'statistics'] 
    entities:
        User:
            # adds 1 custom action, removes 1 custom actions, removes 1 default
            # action and adds two default actions only for this entity
            actions: ['grantPermission', '-statistics', '-show', 'new', 'edit']

The last use case that you comment looks extremely advanced and customized to me. In any case, I think you can easily solve it with a custom action tied to a route. You get the id of the map to edit, execute some code, render your own templates, with your own JavaScript code and then you push some button in your template which sends you to your controller and at the end of it, redirect the user back to the EasyAdmin template.

ogizanagi commented 9 years ago

:+1: for the global @Pierstoval's vision and about considering even built-in actions as custom actions. In fact, that was one of the purposes of my previous suggestion in #95 in which I was trying to cover the largest panel of actions we could imagine. Considering every actions the same way seems more natural, and promotes the bundle extensibility.

But, I think you're introducing, inside the easy_admin configuration, parameters which are only inherent to the actions logic. IMO, the configuration shouldn't know anything about the action type or whatever, but only knowing if this action should be available for an entity, or not.

That's why I suggested some extensions point (the hooks) in the templates, that custom actions could "subscribe" in order to show the appropriated control, at the appropriated location(s). But of course, it will require a more complex internal structure about the way custom actions should be handled.


I'm not fan of the repository option, but I don't have any proper arguments against it for now.

@javiereguiluz : :+1: About the ability to easily remove actions with the - at entity level

Pierstoval commented 9 years ago

@javiereguiluz Your implementation with the - to remove the action sounds really great to me, in fact that's exactly the behavior I'd like to have in the config, and it prevents using another attribute like the override_default_actions I proposed!

If you all agree with it, I think we got our solution :+1:

For the last use about "action types" it was just an idea, and you're right about it: it looks too complex and maybe too specific to be loved by other developers. And the route option can in the end override any "action type", so let's forget about this idea :wink:

ogizanagi commented 9 years ago

@javiereguiluz : If you want to perform some action to the whole entity, you can do that in the code. But that's very strange, because custom actions are displayed as links for each list item. Users will assume that acton affects only to the given record.

@Pierstoval : Export a list of entities in CSV/PDF/TXT/XLS : READ

Exporting capabilities are spread enough in backends to require such a feature. EasyAdmin will not handle every possible formats (maybe at least CSV), so we have to keep the ability for the developers to provide their own custom actions to do that. It cannot be single-record related and I think there are many other usages for entity level actions (stats, advanced search, export, compare, ...).

From @Pierstoval comment, we could extract at least the 3 following actions types :

But, it does not belong to the configuration to state which action should be a single record or entity related one, but to the action itself.

Last point: How would an external bundle provide custom actions in order to reuse them with other backends ? e.g: export to XLS.

Pierstoval commented 9 years ago

I find your comment very good for the action types, as the tree types you noted are probably all we need as an action_type parameter.

For your last point, I don't fully understand the question :confused:

ogizanagi commented 9 years ago

Imagine a third party bundle wishing to add some capabilities to EasyAdmin built-in features, through custom actions. For instance: providing a custom action allowing to export entities to XLS.

How would this third party bundle do that ? A priori, not by extending the AdminController. It brings too many issues due to inheritance. Using routes ? :

User:
    class: AppBundle/Entity/User
    actions:
        - { route: 'export_xls' , label: 'Export to XLS', action_type: 'entity' } 

This will require the user of this third-party bundle to explicitly define the action with the route parameter (for each entity configuration ?!). Ditto for action_type (which again, I don't believe should be in the configuration anyway). Which is not very pleasant, from a DX perspective. The best would be to only have to register an externally provided custom action like this:

User:
    class: AppBundle/Entity/User
    actions:
        - export_xls 
Pierstoval commented 9 years ago

Hmm...

I'm not familiar enough with container build process, but the only thing I have in mind for this is to register a service with a tag, and the only goal of the service will be to return an array of actions that will be added to the back-end global actions, and an array of entities that will be added to the back-end entities.

ogizanagi commented 9 years ago

@Pierstoval : Did you just say "tagged services" ? :smiley_cat:

and the only goal of the service will be to return an array of actions that will be added to the back-end global actions, and an array of entities that will be added to the back-end entities.

Not sure I understood.

Pierstoval commented 9 years ago

Something like this:

# vendor/custom/VendorBundle/Resources/services.yml
vendor_bundle.admin.register:
    class: VendorBundle\Admin\Register
    tags: 
        - { name: easyadmin.register_config }
<?php
namespace VendorBundle\Admin;

use JavierEguiluz\Bundle\EasyAdmin\Configuration\RegisterConfigInterface;

class Register implements RegisterConfigInterface
{
    public function getEntities()
    {
        return array(
            'User' => array(
                'class' => 'VendorBundle\Entity\User',
            ),
        );
    }

    public function getGlobalActions()
    {
        return array(
            'edit_interactive' => array(
                'scope' => 'entity',
                'label' => 'Édition interactive',
                'route' => 'custom_action_edit_interactive',
            ),
        );
    }

}
ogizanagi commented 9 years ago

Again, it will be the responsibility of the user of the third-party bundle to create this service and configure everything then, right ? Seems like this is only a second way to configure easy_admin configuration, through code, on application side, not bundle one.

Pierstoval commented 9 years ago

No, it's the third-party bundle that will need to register this service and add it to its resources, so it's called after configuration computation. In the order it's this:

I'm not familiar enough with tagged services, but I hope it's the expected behavior, and I hope I'm clear enough to be understandable :smile:

ogizanagi commented 9 years ago

Why has your Register class a method getEntities returning the user application's entities if this class is defined in the third-party bundle ?

I see the namespace AppBundle\Admin;, but then, I don't understand how the user is not responsible of creating this class... :confused:

javiereguiluz commented 9 years ago

I think we're complicating everything too much. I'll publish soon a new proposal to provide most (but not all) of your requirements.

Pierstoval commented 9 years ago

@ogizanagi Of course, my example should have been in some kind of vendor bundle, I'm gonna change that in my example

javiereguiluz commented 9 years ago

OK. Here are my latest ideas around actions. Let me know what do you thing about it.


Reference of the different customization strategies

Select the built-in actions to display

By default, EasyAdmin displays all the built-in actions:

If you want to remove any built-in action, use the -action_name syntax:

easy_admin:
    list:
        actions: ['-edit', '-search']
    edit:
        actions: ['-delete']
    show:
        actions:
            # same as above
    new:
        actions:
            # same as above

The actions option can also be defined for entities instead of the entire backend.

Add a simple custom action

easy_admin:
    list:
        actions: ['publish']
    edit:
        actions:
            - { action: 'grantAccess', type: 'method', label: 'Grant' }

The actions option can also be defined for entities instead of the entire backend.

Full action customization

An example of a super complex action customization:

easy_admin:
    list:
        # global actions displayed for all items of the 'list' action of all entities
        actions:
            # built-in options (you can redefine their labels and their behavior)
            - '-show'
            - '-search'
            - '-edit'
            # custom action - implicit options: type = method, label = 'unpublish'|humanize
            - 'unpublish'
            # custom action - implicit options: label = 'publish'|humanize
            - { action: 'publish', type: 'method' }
    edit:
        # global actions displayed for the 'edit' action of all items of all entities
        actions:
            # built-in options (you can redefine their labels and their behavior)
            - 'save'
            - 'delete'
            # custom action - implicit options: type = method, label = 'unpublish'|humanize
            - 'softDelete'
            # custom action - implicit options: none
            - { action: 'save_as_pdf', type: 'route', lavel: 'Save as PDF' }
    show:
        # same as above
    new:
        # same as above
    entities:
        User:
            class: AppBundle/Entity/User
            list:
                actions:
                    # don't display the 'search' action for the 'list' action of this entity
                    - '-search'
                    # don't display the 'show' action for the 'list' action of this entity
                    - '-show'
                    # custom action - implicit options: type = method, label = 'resendRegistrationEmail'|humanize
                    - 'resendRegistrationEmail'
                    # custom action - implicit options: none
                    - { action: 'grantAccess', type: 'method', label: 'Grant' }
                    # custom action - implicit options: none
                    - { action: 'softDelete', type: 'repository', label: 'Delete' }
                    # custom action - implicit options: none
                    - { action: 'update_user_information', type: 'route', label: 'Refresh' }
            edit:
                actions:
                    # override the properties of a custom action for the 'edit' action of this entity
                    - { action: 'softDelete', type: 'repository', label: 'Remove' }
                    # remove this custom action for the 'edit' action of this entity
                    - '-save_as_pdf'
            show:
                actions:
                    # same as above
            new:
                actions:
                    # same as above

The actions option can also be defined for entities instead of the entire backend.


Reference of action types

type: method

Example:

easy_admin:
    list:
        actions:
            - { type: 'method', action: 'grantAccess', label: 'Grant Access' }
            # the following configuration is equivalent
            # - 'grantAccess'

EasyAdmin executes the method of the AdminController that matches the action name: $this->{$method}();

$this->grantAccess();

// ...

// define this method in a controller extending AdminController
public function grantAccess()
{
    $entity = $this->em->find($this->request->query->get('id'));
}

type: route

easy_admin:
    list:
        actions:
            - { type: 'route', action: 'user_grant_access', label: 'Grant Access' }

EasyAdmin generates the following link in the list.html.twig page:

<a href="{{ path(action.action, { entity: entity.name, id: entity.id }) }}">
    {{ action.label }}
</a>

The route of the action can be replied by any controller of your application:

/**
 * @Route('user_grant_access')
 */
public function grantAccessAction(Request $request)
{
    $user = $this->get('user_repository')->find($request->query->get('id'));

    // ...

    // finish the custom action redirecting the user to the 'list' action
    return $this->redirectToRoute('admin', array(
        'action' => 'list',
        'entity' => $request->query->get('entity'),
    ));
}

type: repository

easy_admin:
    list:
        actions:
            - { type: 'repository', action: 'grantAccess', label: 'Grant Access' }

EasyAdmin executes the method grantAccess() of the repository associated with the given entity, and passes the id of the selected entity as argument:

$repository = $this->em->getRepository($this->entity['class'])
$repository->{$action}($this->request->query->get('id'));
$this->em->flush();

Example:

$repository = $this->em->getRepository('AppBundle\Entity\User');
$repository->grantAccess(12);
$this->em->flush();

type: entity

easy_admin:
    list:
        actions:
            - { type: 'entity', action: 'grantAccess', label: 'Grant Access' }

Very similar to the repository type, but the method is executed on the Doctrine entity itself, not on its repository:

$entity = $this->em->getRepository($this->entity['class'])->find($this->request->query->get('id'));
$entity->{$action}();
$this->em->flush();

Example:

$entity = $this->em->getRepository('AppBundle\Entity\User')->find(12);
$entity->grantAccess();
$this->em->flush();

Reference of action configuration options


Action inheritance behavior

We have:

Imagine that the backend configuration is as follows:

easy_admin:
    list:
        actions: ['global_action']
    entities:
        User:
            list:
                actions: ['local_action']

What are the actions of User?

Implicit action inheritance

Entities inherit global backend actions and they use -action_name syntax to avoid inheriting unwanted actions:

easy_admin:
    list:
        actions: ['global_action']
    entities:
        User:
            list:
                actions: ['local_action', '-global_action']

Explicit action inheritance

On the contrary, if entities don't inherit the global actions, they should repeat them in case they want to display them:

easy_admin:
    list:
        actions: ['global_action']
    entities:
        User:
            list:
                actions: ['local_action', 'global_action']

In my opinion the first behavior (implicit) is more intuitive than the second one (explicit).

Pierstoval commented 9 years ago

For the two last points, I think the first of them is the only one that should be kept. Using the - notation should become intuitive with this behavior.

About the action types, I don't think that the types repository or entity would be used most of the time... Even if they're handy, they're quite hard to understand the first time. A custom route is obvious for example.

But it may be personal, I don't know what would think other people about these specific types. method (which implies extending the controller for custom actions) and route (which is handier) are good enough. Let me just share my feeling about it:

Remember that we don't need to "think for users", because the final users will be developers, which, most of the time, will choose this bundle for one of 2 reasons (or both): easy to setup, easy to customize. Performances and adaptability come in background, that's precisely the reason why I abandoned Sonata suite and chose EasyAdmin. A simple back-end with the actual functions EasyAdmin now offers is "never enough" when creating an application (and it's the same for any other bundle) and many developers that are in companies that develop complex apps would want to always have a hand on every feature. The simple entity and repository types will only have one feature, and worse than that, they can only apply on one entity at a time, and is a simple "customized edition", whereas method and route offer all possibilities provided by the framework, and the developer can do whatever he wants. It may be tough to say it, but I don't think that they would be used whatsoever. Nonetheless they're both very smart ideas! It just sounds too (very very very) much specific to me.

For the rest, I don't have anything to say: it's great :+1:

Pierstoval commented 9 years ago

Sorry for double-posting, I'd prefer two notification mails than that you read the first message without reading the "edit" part.

I really forgot something...

Why did you "changed" the actions list notation?

I mean, the "old" one was this like:

easy_admin:
    actions:
        - { label: "edit", type: "method" }
        - { label: "list", type: "method" }
        - { label: "custom", type: "route", action: "custom_route" }

and the new one is like:

easy_admin:
    list:
        actions:
            - { label: "edit", type: "method" }
            - { label: "list", type: "method" }
            - { label: "custom", type: "route", action: "custom_route" }

First, this notation implies a huge BC break (unless you accept to merge two big complex arrays, but I don't think it's worth it...), and second, a really simple and tiny show_in array would be easier to implement in the current back-end:

easy_admin:
    actions:
        - { label: "edit", type: "method", show_in: [ 'show', 'list' ] }
        - { label: "list", type: "method", show_in: [ 'new', 'edit', 'show', 'delete' ] }
        - { label: "custom", type: "route", action: "custom_route", show_in: [ 'list' ] }

What do you think about this?

javiereguiluz commented 9 years ago

@Pierstoval thanks for your comments ... and for reading my long comments :smile:

ogizanagi commented 9 years ago

Reference of action types

@Pierstoval : About the action types, I don't think that the types repository or entity would be used most of the time... Even if they're handy, they're quite hard to understand the first time. A custom route is obvious for example. But it may be personal, I don't know what would think other people about these specific types. method (which implies extending the controller for custom actions) and route (which is handier) are good enough. Let me just [...]

Exactly my thoughts... I don't find any value to those helpers method. Not intuitive at first. Not useful subsequently, as everything could be done the exact same way, and very simply with the methods and route types.

Action inheritance behavior

Implicit way :+1:

Reference of the different customization strategies

As you said, it's "An example of a super complex action customization"...still, only a little part of it. It feels really really heavy and unintuitive to me. :confused:

I really see custom actions as a primary feature, and something that should be "shippable" in a third-party bundle. The user should not have more concerns than configuring the entities for which those actions are enabled in easyadmin config. The action by itself must be able to choose where the buttons/links are rendered (which page, and where in the page). The only responsibility for the user, is to register the action for the desired entities.

For more options, if needed, the action itself (I mean the bundle) should provided a way to configure specific actions' parameters. It's not the EasyAdminBundle responsibility anymore. Same for the target attribute. Only the action should be aware of this. The user shouldn't be able to register a custom action dedicated to single records as an entity action.

Needless to say, implementing such a way to handle actions rendering will be kind of heavy in the code/templates too. IMO by wishing not to overcomplicate the bundle by extending constantly the configuration, and rely on it for everything, we're actually overcomplicating the way we handle features, instead of relying on the extensibility and the developers skills.

Yes, EasyAdmin should be easy to use for most simple backends. The 5 minutes it takes to generate the basic admin backend is amazing. But I think it must also limit its responsibilities, by providing extension points that will help reducing the internal code & templates complexity, and trust other developers' skills for most advanced features.

BTW, here :

easy_admin:    
    edit:
        actions:
            - { action: 'grantAccess', type: 'method', label: 'Grant' }

I think the action attribute must be renamed name in the action declaration, or we should differentiate the action's name, and its value (which is a route name or a method name). What do you think ?

Pierstoval commented 9 years ago

I don't know, maybe a quatuor name, label, type and value|resource|action might sound more explicit?

javiereguiluz commented 9 years ago

@ogizanagi I've read your comments carefully (as I always do with your comments) and I've finally made my mind about custom actions. I admit that your proposal about "independent actions" is not wrong but I believe that it's too complicated to fit EasyAdmin philosophy. The same goes for your thoughts about third-party bundles and "reusable actions". You are not wrong, but right now we don't care at all about third-party bundles. EasyAdmin is a monolithic piece of software, and I proudly say that as a good feature, not as something wrong.

All in all, I'm going to implement what I explained in this comment. However, I'll make the following changes to simplify things:

Pierstoval commented 9 years ago
  • The main constraint of your custom actions is the template used to display them: this is hardcoded and cannot be modified: some actions are displayed as buttons and other as links, depending on the page and the type of action.

Do you have a concrete example of this behavior? I don't fully get it :confused:

javiereguiluz commented 9 years ago

@Pierstoval I was referring to "the thing" that users click to activate your action. Sometimes is a button and sometimes is a link (e.g. in the listings of items). You cannot change that to do fancy things. For example, the search action is neither a button nor a link: it's a form. You cannot do that with your actions: you'll always get a button or a link.

Pierstoval commented 9 years ago

So if I resume:

We're missing the non-entity related actions, such as dashboards.

javiereguiluz commented 9 years ago

@Pierstoval :+1: you explained it perfectly!!

Pierstoval commented 9 years ago

Great then !

For non-entity related actions, I think we'll need to setup an easy method later, because I think these kind of actions will have their own place in the navbar...

Pierstoval commented 9 years ago

By the way, I think, with some steps back over the situation, that making a difference between entity and item is not very easy. Maybe something like list_head and object_view would be easier to understand, because in fact this attribute is a link_position or button_placement. What do you think about it?

javiereguiluz commented 9 years ago

@Pierstoval to be honest, I'm waiting for the implementation to decide if this option remains or if it's removed. At the end, we'll probably start with "item actions" for list and "entity actions" for the rest of pages.

ogizanagi commented 9 years ago

EasyAdmin is a monolithic piece of software, and I proudly say that as a good feature, not as something wrong.

Yeah, I don't see this as something wrong neither. You want to provide something strong and stable. Something far away and beyond the complexity of other admin generation bundles. I got it (this is not sarcasm). I'm glad to talk with you about all of this, and I thank you for considering all of my comments with so much attention. I only fear that this project is closing in onto itself, relying on its great capabilities of settling a very nice and clean CRUD backend in a snap, to argue that it should also do more, only by itself. You realized in your comment:

The main constraint of your custom actions is the template used to display them: this is hardcoded and cannot be modified

that this approach is quite limiting the developer ability to customize the backend, an there are many things that will be kept "hardcoded" that way.

The main drawback of other admin generators is the time and the effort necessary to build the very basis of an admin backend with our entities. Which EasyAdmin bundle already get rid of. Now it should not limit itself by providing implementations of specific behaviors, but instead providing enough extension points and interfaces for the developer to develop the whole admin backend around the easy admin core.

If you're not in this mindset for now, I don't even think that introducing actions types nor other advanced features about actions will be relevant. Therefore, your first proposal about only item actions perfectly fits the bundle's perspective, and I apologize for having continuously looped on my own concerns. Everything else might be done in another application part (we should at least provide a way to extend the bundle's layout more efficiently if the developer wants to keep consistency between the different backend parts).

Anyway, lately I was thinking about a system similar to symfony's form themes in order to override the easyadmin types widgets and other parts only from templating (The TwigExtension will not generate the templating code anymore but call the proper widget from a theme template instead). The same should have been applicable to actions. But it seems really really complicated to achieve such a thing as powerfully as the form theming.

Pierstoval commented 9 years ago

With EasyAdmin it should be easy to customize forms rendering : create your own "form definition" like the form_div_layout.html.twig in the framework, and add it in the Twig config, it's the simplest way to do it, and it works with EasyAdmin. Plus, it also works when creating custom form types and adding their layout in your twig config.

For the rest, I agree in the fact that EasyAdmin should be extensible in every possible way, and in fact, I'm wondering if we had to create a whole new bundle for it, that adds features to EasyAdmin (exactly like what SensioFrameworkExtraBundle does to the FrameworkBundle, for instance)

javiereguiluz commented 9 years ago

I apologize for having continuously looped on my own concerns

No need to apologize at all. In fact, we must be very "selfish" and demand support for solving the needs of our backends (e.g. I added boolean flip switches because I selfishly need them in some of my backends). So please, keep asking and demanding anything that you (real backend users) need for your backends. The only thing we should keep in mind is that we target the 80% simplest backends. If your needs are very complex, go for the powerful SonataAdmin bundle.

Yes, we're going to allow to use your own themes, but let's do one thing a time. My current personal roadmap is as follows:

ogizanagi commented 9 years ago

@Pierstoval : Yep, I'm actually talking about that...for non-form elements: entity fields that are rendered through the TwigExtension, custom actions links/buttons, any dynamically determined and generated part of the templating... The idea is that those elements are in a easyadmin_theme_layout.html.twig, the same way as in form_div_layout.html.twig, and if the user isn't satisfied of how is currently rendered the boolean entity field or its custom actions buttons on the list page, he can provide his own theme.

As I said, it's a complex feature (to implement, not to use. You can have a look at how works the Symfony's FormExtension and FormThemeTokenParser) which may provide a not so much exciting result. But before digging into the code, I found this principle interesting for EasyAdmin theming.

Pierstoval commented 9 years ago

@javiereguiluz => For the Doctrine Associations, work will be quite hard, but I still think that we can just use configuration values on "how to render the relation", combined with specific twig views for how to represent it. Managing the form is easier if you need an embedded form, a little JS script for *ToMany relations (in order to duplicate the embedded form) , some loops, and in theory it does not sound so hard. And it may work on each relation type. (remember this comment on #133 )

@ogizanagi : Using a form theme is the easiest way to do it whatsoever, in my opinion. You create a theme, use it, and if you want to customize it, some docs about template overridance in Resources/EasyAdminBundle/views/easyadmin_form_theme.html.twig , or even as a config parameter form_theme: AppBundle:Form:admin_form_theme.html.twig and in the admin views you load the form_theme dynamically from the config parameter...

I think I might theorize very fast and very much about all of this, but I make the best I can to propose a very good solution to each needs with the most extensible way possible :confused:

That said, I think this issue should be renamed "State of the project" :wink: :smile:

javiereguiluz commented 9 years ago

I'm glad to finally close this issue as solved by #202

By the way, impressive stats of the related PR:

custom_actions_pr

Pierstoval commented 9 years ago

This is a huge step, you made it perfectly, and I'm really glad that this bundle is becoming really big in terms of features!