We use the SonataAdminBundle with our Symfony2 application. When editing an entity I want to add an own action to the dropdown menu which is located in the top right corner, but I have no idea how this works.
I know I can add own routes via configureRoutes(RouteCollection $collection) and how to add batch actions or add own actions behind entities in the list view, but how can I add an own link in the actions dropdown in the edit view?
It is basically just a link like "Show me this entity in the frontend", so no big logic is needed.
One way would be to override the template that is used when editing. Now, what you need to do is:
Create new directory (if you already haven't) in app/Resources called SonataAdminBundle. Inside, create another one called views. This would create a path like app/Resources/SonataAdminBundle/views. This is Symfony basic template overriding. You can read more on that subject here.
Now, you should copy the original template following the same path as it is, inside the original bundle. The template file we are interested here is located in sonata-project/admin-bundle/Resources/views/CRUD/base_edit.html.twig. This means that you have to create another folder inside views (the one we just created in app, called CRUD. So, now we have to following path app/Resources/SonataAdminBundle/views/CRUD. Paste the template (base_edit.html.twig) inside and we can start editing.
Keep in mind that the following template is used in every edit action you have. So it's up to you whether you want to display that link in every edit_action or not. I will show you 1 way to limit that for specific action.
The block you gonna edit is {% block actions %} which is responsible for rendering the dropdown. This is how it should look now:
{% block actions %}
<li>{% include 'SonataAdminBundle:Button:show_button.html.twig' %}</li>
<li>{% include 'SonataAdminBundle:Button:history_button.html.twig' %}</li>
<li>{% include 'SonataAdminBundle:Button:acl_button.html.twig' %}</li>
<li>{% include 'SonataAdminBundle:Button:list_button.html.twig' %}</li>
<li>{% include 'SonataAdminBundle:Button:create_button.html.twig' %}</li>
{% endblock %}
Now all that's left to do is insert your link after last <li> tag.
{% if admin.id(object) is not null and app.request.get('_route') == 'my_route' %}
<li>
View in Frontend
</li>
{% endif %}
admin.id(object) will return the current ID of the item you edit. app.request.get('_route') will return the route of your edit action. You can remove that if you want your link to be displayed in all edit actions. Change View in Frontend with your route name using admin.id(object) and you should be good to go.
In your admin class, override the following method:
public function getActionButtons($action, $object = null)
{
$list = parent::getActionButtons($action, $object);
$list['upload'] = [
'template' => ':admin:my_upload_button.html.twig',
];
return $list;
}
This will add a custom action button on all the pages of this admin. You can add any logic in here to decide which pages ($action-s) you want to display the button on.
You can do what you want in the template, but just to complete my example and show the connection with my custom action:
<li>
<a class="sonata-action-element" href="{{ admin.generateUrl('upload') }}">
<i class="fa fa-cloud-upload" aria-hidden="true"></i>
Upload stuff
</a>
</li>
Another way would be to override the method generateObjectUrl() in your object's admin class.
/**
* #see \Sonata\AdminBundle\Admin\Admin::generateObjectUrl()
*/
public function generateObjectUrl($name, $object, array $parameters = array(), $absolute = false)
{
if ('show' == $name) {
return $this->getRouteGenerator()->generate('your_route_to_public_facing_view', [
'id' => $this->getUrlsafeIdentifier($object),
], $absolute );
}
$parameters['id'] = $this->getUrlsafeIdentifier($object);
return $this->generateUrl($name, $parameters, $absolute);
}
And that's it. No mucking with templates. And no template code that will run on every other admin.
To get the link to appear automatically, you will have to add something to the $showMapper via configureShowFields(). (If anyone knows a better way, please do tell.)
Overriding generateObjectUrl() has another bonus: If you display a show button on the $listMapper, the URL there will be updated there as well.
Edited to say: since this overrides the show route, you will no longer be able to use that built-in feature. That's ok for me since I need to view my object with all the front-end css and js loaded.
Related
I am begginer and I have little problem.
I have:
I have one primary menu with links to Controller main actions (index) and one secondary menu with links to other actions like create / edit / delete.
My problem is:
I don't know how to tell twig, that links in primary menu are active when any action in that controller is called.
In secondary menu I have:
{% set route_name = app.request.attributes.get('_route') %}
<a class="{% if route_name == item.route %} active {% endif %}">Link</a>
I have tried:
I tried to do it with prefixes on Controller, but it didn't worked.
Any hints or tips please? Thank you!
If your menu has only 2 nested levels, you can set a #Route annotation in the whole controller:
/**
* #Route(path="admin/pegass", name="admin_pegass_")
*/
class PegassController extends BaseController
{
For every action in your controller, you just need to set the end of the name and path:
/**
* #Route(name="list_users", path="/list-users")
*/
public function userList()
{
Finally, when rendering the first level of your menu, you can use:
{% set route_name = app.request.attributes.get('_route') %}
<a class="{% if route_name[0:13] == 'admin_pegass_' %} active {% endif %}">Link</a>
For more complex menus, breadcrumbs are not trivial, it will be easier to create an object representation of it in order to traverse it and perform operations in a more generic way (you can use a bundle like KnpMenu).
I am new to drupal and I tried to add a simple image as static in the html.html.twig page.
To display this image in the home page only I added this condition
{% if is_front %}
{% endif %}
What is the condition to add it in another specific page example /contact/
I tried this code inside the page but it does not work, should I wrap it with some codes?
if(drupal_valid_path('contacts') == 1) //this exists
{
print_r('Exists!');
}
Please help! Many thanks.
Your question is mixing twig with a drupal 7 function. I suppose anyway you're on D8/D9.
I also suppose the "contacts" page is a node. If it is somethings else - e.g. a view page - instead of checking for the node, you may have to check the route.
There are various approach, two possible would be:
A hook preprocess where you load the node if exists
function hook_preprocess_html(&$variables) {
$node = \Drupal::routeMatch()->getParameter('node');
if ($node instanceof \Drupal\node\NodeInterface) {
$variables['node'] = $node;
}
}
And the in the twig check the id of the node is the right one
override the html.html.twig for the specific node, e.g. html--node--10.html.twig. In this case I'd suggest not to duplicate all the content of the base html.html.twig, but to use the embed tag to override only the appropriate block.
I am wondering if there is a easy way to implement a group view for datasets grouped by its "root" element.
Lets say i have a "person" entity. Many persons can have a "root" person they belong to. So i would add a reference to the person entity itself.
Now i don't want to show every person in the flat list view. Instead i want to display only every root-person and with clicking this dataset a accordion opens with the subordinated person entities ... how is that possible?
It would also be fine without a accordion, it can be enough if the subordinated entities are indented a bit ...
Can somebody give me a clue which approach i should follow? I would be the if i can reuse the most of the sonata admin functionality, especially the templates ...
Thanks
Was solving something similar in past. Got that working in few steps:
Override first field in your Datagrid
With this code you say that you want use my_custom_template.html.twig to render this field (in your admin class).
protected function configureListFields(ListMapper $list)
{
$list->add('yourFirstField', null, ['template' => 'my_custom_template.html.twig'])
}
Enable filtering for yourFirstField
Prepare filtering for your parent field (in your admin class)
protected function configureDatagridFilters(DatagridMapper $filter)
{
$filter->add('parent');
}
Write custom template for first field
Then in your custom template you use to set up filter value on click on link.
{% extends 'SonataAdminBundle:CRUD:base_list_field.html.twig' %}
{% block field %}
{% if object.isRoot %}
{{ object.name }}
{% else %}
{{ object.name }}
{% endif %}
{% endblock %}
Later on you can override break breadcrumbs builder service (https://sonata-project.org/bundles/admin/master/doc/reference/breadcrumbs.html) and create nice path in breadcrumbs, so user can go navigate.
I have a Project entity, which has a controller defining many routes:
projects/1
projects/1/foo
projects/1/bar
I need a service to provide the current project. The use case is that I have dependencies in my base twig templates which need to know the current project. i.e. a dropdown project selector that is outside the context of the template the current controller is serving.
I've tried creating a service getting route info with $container->get('router.request_context');, however, that only provides a path. I don't want to have to parse the path string if I don't have to.
What is the most proper approach?
If I understood you correctly the solution for your problem is rendering/embedding controller. Of course it's simplest, yet somehow acceptable solution for rendering parts of html with some custom logic apart from current template.
You can read about rendering/embedding controllers.
Some snippets...
Define controller:action (obviously the logic in my example is pretty straight forward):
/**
* Generate select input field.
*
* #Route("/widget", name="project_widget")
* #Method("GET")
*/
public function widgetAction()
{
$repo = $this->getDoctrine()
->getManager()
->getRepository('AppBundle:Project');
// #NOTICE: Wee need master request info, because our widget
// is rendered and called as subrequest.
$masterRequest = $this->get('request_stack')->getMasterRequest();
// #NOTICE: You need second parameter as default value in case there is no id in masterRequest route.
$currentProjectId = $masterRequest->get('id', 0);
$currentProject = $repo->find($currentProjectId);
$projects = $repo->findAll();
return $this->render('project/widget.html.twig', [
'currentProject' => $currentProject,
'projects' => $projects,
]);
}
Then you need to create the template project/widget.html.twig for it:
<div class="widget_project_selection">
{% if projects is not empty %}
<select name="widget_project_selection" id="widget_project_selection">
<option value="">None</option>
{% for project in projects %}
<option value="{{ project.id }}"
{# #NOTICE: If this is current project, select it #}
{{- currentProject and project.id == currentProject.id
? 'selected="selected"'
: '' -}}
>
{{- project.title -}}
</option>
{% endfor %}
</select>
{% else %}
<span>{{ 'Sadly, no projects yet :('|trans }}</span>
{% endif %}
</div>
and at last (but not least) render it somewhere like in base.html.twig:
{{- render(controller('AppBundle:Project:widget')) -}}
I've created for you a Github repo as reference. It's a small Symfony app with similar setup. You can even run it if you like, don't forget about dependencies and database update thou. Entry point is /app_dev.php/project/
Take a look at widgetAction, widget template and example usage in base.html.twig.
EDIT: But that's not everything. You said you need a service. If for some reason rendering/embedding controller is not an option for you or you really whant to use a Service (as in Dependency Container) you can extend Twig and use the full power of services.
I've also implemented a Twig Filter as example to show you the real power of Twig Extensions in here and here (usage in templates).
Check out Twig Extension and Extending Twig for more info about Twig Extensions.
Also check out service.yml for service and extension definitions - if you are not using Symfony3.3+, there will be some additional work to do - defining service and extension directly.
In your controller, you can use type hinting to load the "current" entity via the route.
For example:
#myrouter.yml
current_project:
path: /projects/{project}
Separately, your controller...
//mycontroller.php
public function myControllerAction(Request $request, Project $project)
{
//$project is entity (assuming valid) loaded via the route above
return $this->render('mytemplate.twig', ['project' => $project]);
}
I'm wondering how to get the current page name, basically 'just' the last parameter in the route (i.e. /news or /about). I'm doing this because I want to be able to have the current page in the navigation highlighted.
Ideally, I'd like to store the current page name in a global variable so that in Twig I can just compare the current page name against the link and add a class accordingly.
I can't figure out how to add the current page name to a global variable though. I've tried using something like this:
$app['twig']->addGlobal('current_page_name', $app['request']->getRequestUri());
at the top of my app.php file, but an 'outside of request scope' error. But I wouldn't like to have to include this in every route.
What's the best way to do this?
If you put it into an app-level before middleware like this, that'll work:
$app->before(function (Request $request) use ($app) {
$app['twig']->addGlobal('current_page_name', $request->getRequestUri());
});
The "page name" part of your question is unclear, are you looking for the current route's name? You can access that via $request->get("_route") even in the before middleware, as it gets called when routing is already done.
You could also generate navigation list directly in stand alone nav twig template. And then import it in to the main template. Then you would only have to get silex to pass to the view the current page identifier. Simplest way... for example from Silex you would have to pass in the "path" variable to your view. Probably it would more convenient to to fetch nav_list from database and pass it in to twig template as global array variable instead. However this example is the simplest you could get to do what you intend.
nav.twig
{% set nav_list = [
["./", "home"],
["./contact", "contact"],
["./about", "about us"]
{# ... #}
] %}
{% set link_active = path|default("") %}
{% for link in nav_list %}
<li><a href="{{ link[0] }}" class="{% if link[0] == link_active %} activeClass {% endif %}" >{{ link[1] }}</a></li>
{% endfor %}
app.php
//...
$app->match('/about', function (Request $request) use ($app) {
return $app['twig']->render('about.twig', array(
'path' => './'.end(explode('/', $request->getRequestUri()))
));
});
//...