Make a list field editable when this field is a many_to_one type using in Sonata-project Symfony - sonata-admin

My entity
/**
* #ORM\ManyToOne(targetEntity="Estat", inversedBy="temes")
*/
private $estat;
public function setEstat(\Ncd\ForumBundle\Entity\Estat $estat = null)
{
$this->estat = $estat;
return $this;
}
My admin
protected function configureListFields(ListMapper $listMapper)
{
//$estats=$this->getEstatsPossibles()->toArray();
$estats=array();
foreach($this->getEstatsPossibles() as $estat)
{
$estats[$estat->getId()]=$estat->getNom();
}
$listMapper
->add('estat', 'choice',['editable' => true,'choices'=> $estats])
I'd like to make estat field editable in the list grid. Doing it on this way I get make it editable, a combobox appears but when I chose an option I get an exception because setEstat function of my entity does not recive an Estat entity, but a string (the array's key).
Trying
->add('estat', 'many_to_one',['editable' => true,'choices'=> $estats])
Only appears a link to the entity without any possibility to change.
Is it possible?

Waiting for a better and cleaner solution I'v solved this injecting an entityManager in my entity following the solution of this answer:
Get entityManager inside an Entity
Then, in my entity I've changed setEstat function:
public function setEstat( $estat = null)
{
if (is_object($estat) && get_class($estat)=='Ncd\ForumBundle\Entity\Estat')
{
$this->estat=$estat;
} else {
$estat_o=$this->em->getRepository('Ncd\ForumBundle\Entity\Estat')->find((int)$estat);
$this->estat = $estat_o;
}
return $this;
}

Related

How would I change entity labels?

I use Symfony4 and Sonata admin. When I use ModelListType it worked as shown on the screenshot below.
How would I change entity item machine name: ('App\Entity\Product:000000003aaca7040000000026c8b335') to entity item field 'name' value?
My code for this field is:
#/project/src/Admin/ProductAdmin.php
...
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('name')
->add('category', ModelListType::class);
}
...
I have solve this by adding __toString() method to entity:
public function __toString(){
return $this->getName();
}
Just return the value directly by name instead of calling the getter method like,
public function __toString()
{
return $this->name;
}

.NET Tag Helper to replicate #Html.DisplayFor

I'm discovering .Net Core Tag Helpers and I was just curious to know if there are any tag helpers that replicate the #Html.DisplayFor. I think that the label tag helper replicates #Html.DisplayNameFor since it shows the property name on a model passed to the page, but is there an equivalent for #Html.DisplayFor for displaying a property value?
I'm assuming there isn't because in the microsoft .net core tutorials, razor pages that need to display the property value rather than the property name use the HTML helpers.
First, the tag helper is actually the "label asp-for". You can create a new tag helper that is a "label asp-text" helper.
Another option is to use another tag such as span and create a custom "span asp-for" tag helper.
Here is a simple span implementation:
[HtmlTargetElement("span", Attributes = FOR_ATTRIBUTE_NAME, TagStructure = TagStructure.NormalOrSelfClosing)]
public class CustomSpanTagHelper : InputTagHelper
{
private const string FOR_ATTRIBUTE_NAME = "asp-for";
public CustomSpanTagHelper(IHtmlGenerator generator) : base(generator)
{
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
var metadata = base.For.Metadata;
if (metadata == null)
{
throw new InvalidOperationException(string.Format("No provided metadata " + FOR_ATTRIBUTE_NAME));
}
if (!string.IsNullOrWhiteSpace(metadata.Description))
{
output.Content.Append(metadata.Description);
}
if (metadata.IsEnum)
{
var description = (this.For.Model as Enum).GetDescription();
output.Content.Append(description);
}
base.Process(context, output);
}
}
You will need to register your custom tag helper in your _ViewImports.cshtml like this: (don't forget to rebuild)
#namespace MyProject.Web.Pages
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#addTagHelper MyProject.Web.TagHelpers.CustomSpanTagHelper, MyProject.Web <-- custom import

ReactiveUI "Compelling Example" how to refresh the search results

My question is in regards to the "Compelling Example" given for ReactiveUI where as a person types in a search bar, the search occurs asynchronously. Suppose though I wanted to provide my user with a way to refresh the current search results. I could just ask them to backspace in the search bar and retype their last character. However, they are asking for a "Refresh" button because it's not obvious to them how to refresh the current results.
I can't think of how to do this within the context of the example:
public class TheViewModel : ReactiveObject
{
private string query;
private readonly ObservableAsPropertyHelper<List<string>> matches;
public TheViewModel()
{
var searchEngine = this.ObservableForProperty(input => input.Query)
.Value()
.DistinctUntilChanged()
.Throttle(TimeSpan.FromMilliseconds(800))
.Where(query => !string.IsNullOrWhiteSpace(query) && query.Length > 1);
var search = searchEngine.SelectMany(TheSearchService.DoSearchAsync);
var latestResults =
searchEngine.CombineLatest(search, (latestQuery, latestSearch) => latestSearch.Query != latestQuery ? null : latestSearch.Matches)
.Where(result => result != null);
matches = latestResults.ToProperty(this, result => result.Matches);
}
public string Query
{
get
{
return query;
}
set
{
this.RaiseAndSetIfChanged(ref query, value);
}
}
public List<string> Matches
{
get
{
return matches.Value;
}
}
}
Does anyone have any suggestions on how I could capture a command from a button and re-execute the existing search without clearing out their current search text?
You can merge the existing observable of Query changes with a new observable that returns the current Query when the refresh button is pressed.
First a command for the refresh button:
public ReactiveCommand<Unit, String> Refresh { get; private set; }
Then you create the command and assign it, and create a merged observable of the two observables:
Refresh = ReactiveCommand.Create<Unit, String>(() => Query);
var searchEngine = Observable.Merge(
this.ObservableForProperty(input => input.Query).Value().DistinctUntilChanged(),
Refresh)
.Throttle(TimeSpan.FromMilliseconds(800))
.Where(query => !string.IsNullOrWhiteSpace(query) && query.Length > 1);
The rest can stay unchanged.

Sonata Admin Bundle: show total count of collection on list view

Is there any way to show total count of collection on list view? Imagine that there is a user that can have many links. How can I show total links count on list view?
Show field it is quite easy, there is solution for sorting by this virtual field.
Entity/Some.php more about count here Extra Lazy Associations
public function getCommentsCount()
{
return $this->getComments()->count();
}
SomeAdmin.php override createQuery and configure list field
public function createQuery($context = 'list')
{
$query = parent::createQuery($context);
if ('list' === $context) {
$rootAlias = $query->getRootAliases()[0];
//...
$parameters = $this->getFilterParameters();
if ('getCommentsCount' === $parameters['_sort_by']) {
$query
->leftJoin($rootAlias.'. comments', 'cm')
->groupBy($rootAlias.'.id')
->orderBy('COUNT(cm.id)', $parameters['_sort_order'])
;
}
//...
}
return $query;
}
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('id')
//...
->add(
'getCommentsCount',
null,
[
'sortable' => true,
'sort_field_mapping' => ['fieldName' => 'id'],
'sort_parent_association_mappings' => [],
]
)
//....
}
service.yaml add "simple" paginator (count does not work correctly)
tags:
- { name: sonata.admin, pager_type: "simple", ...
Reasons:
subquery in orm join is not allowed
subquery in orm orderBy is not allowed
HIDDEN field does not work
\Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery::getFixedQueryBuilder
(// for SELECT DISTINCT, ORDER BY expressions must appear in idxSelect
list)
My answer is similar to Khalid (above) but has some key differences.
If you wrap the collection in a count( $entity->getLinks() ) then this will issue a query which returns every link association.
The downside of this is that if you have 1000s of Links associated, the memory resources required will need to be sufficient for hydrate each entity. (Which can be huge if you have thousands of different entities).
Instead, you should mark your Entities as EXTRA_LAZY and then use the --$entity->getLinks()->count()` method which will not do any hydration, instead it will only issue the COUNT queries.
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/extra-lazy-associations.html
So do the following:
/**
* #ManyToMany(targetEntity="Links", mappedBy="whatever", fetch="EXTRA_LAZY")
*/
public $links;
Then you can call:
public function getTotalLinks(){
return $this->getLinks()->count();
}
And it will be super quick.
with Sonata 3.**
in somwhere in Admin***.php script for listing all fields:
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
//...
->addIdentifier('getCommentsCount', IntegerType::class, [ 'label' => '#Comments'])
;
}
Where in Entity i written something like this:
public function getCommentsCount()
{
return $this->comments->count();
}
that works for me )
Yes you can show the total count of links for each user, i assume you have arraycollection of links defined in your user entity, define a property named as $totalLinks and in getter of that property return count of links something like below
class User{
public $totalLinks;
public function getTotalLinks(){
return count($this->getLinks());
}
}
and then in your configureListFields() you can add $totalLinks property
protected function configureListFields(ListMapper $list)
{
$list
->add('...')
->add('...')
->add('totalLinks');
}
Found answer here:
SonataAdminBundle custom rendering of text fields in list
I'm using Sonata 2.3 so TWIG template should be like:
{% extends admin.getTemplate('base_list_field') %}
{% block field %}
{{ value|length }}
{% endblock %}

Custom accessors Eloquent Model

I have a Eloquent Model and I want to create a customized toArray method...
class Posts extends Model {
public function scopeActives($query)
{
return $query->where('status', '=', '1');
}
public function toCustomJS()
{
$array = parent::ToArray();
$array['location'] = someFunction($this->attributes->location);
return $array;
}
}
//In my controller:
Posts::actives()->get()->toArray(); //this is working
Posts::actives()->get()->toCustomJS(); //Call to undefined method Illuminate\Database\Eloquent\Collection::toCustomJS()
How can I override the toArray method or create another "export" method?
get() actually returns a Collection object which contains 0, 1, or many models which you can iterate through so it's no wonder why adding these functions to your model are not working. What you will need to do to get this working is to create your custom Collection class, override the toArray() function, and also override the function in your model responsible for building that collection so it can return the custom Collection object.
CustomCollection class
class CustomCollection extends Illuminate\Database\Eloquent\Collection {
protected $location;
public function __construct(array $models = Array(), $location)
{
parent::__construct($models);
$this->location = $location;
}
// Override the toArray method
public function toArray($location = null)
{
$original_array = parent::toArray();
if(!is_null($location)) {
$original_array['location'] = someFunction($this->location);
}
return $original_array;
}
}
Overriding the newCollection method on your models
And for the models you wish to return CustomCollection
class YourModel extends Eloquent {
// Override the newCollection method
public function newCollection(array $models = Array())
{
return new \CustomCollection($models, $this->attributes['location']);
}
}
Please note this may not be what you are intending. Because a Collection is really just an array of models, it's not good to depend on the location attribute of a single model. Depending on your use-case, it's something that can change from model to model.
It might also be a good idea to drop this method into a trait and then just use that trait in each model you wish to implement this feature in.
Edit:
If you don't want to go through creating a custom Collection class, you can always just do it manually each time...
$some_array = Posts::actives()->get()->toArray();
$some_array['location'] = someFunction(Posts::first()->location);
return Response::json($some_array);

Resources