Added custom choice form type for tree structure entities.

This commit is contained in:
Jan Böhmer 2019-08-13 23:04:06 +02:00
parent 568367b59e
commit cfa807c621
7 changed files with 203 additions and 16 deletions

View file

@ -34,6 +34,7 @@ namespace App\Form\AdminPages;
use App\Entity\Base\NamedDBElement; use App\Entity\Base\NamedDBElement;
use App\Entity\Base\StructuralDBElement; use App\Entity\Base\StructuralDBElement;
use App\Form\Type\StructuralEntityType;
use FOS\CKEditorBundle\Form\Type\CKEditorType; use FOS\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
@ -70,8 +71,8 @@ class BaseEntityAdminForm extends AbstractType
'attr' => ['placeholder' => 'part.name.placeholder'], 'attr' => ['placeholder' => 'part.name.placeholder'],
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]) 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ])
->add('parent', EntityType::class, ['class' => get_class($entity), 'choice_label' => 'full_path', ->add('parent', StructuralEntityType::class, ['class' => get_class($entity),
'attr' => ['class' => 'selectpicker', 'data-live-search' => true], 'required' => false, 'label' => 'parent.label', 'required' => false, 'label' => 'parent.label',
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'move', $entity), ]) 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'move', $entity), ])
->add('not_selectable', CheckboxType::class, ['required' => false, ->add('not_selectable', CheckboxType::class, ['required' => false,

View file

@ -33,6 +33,7 @@ use App\Entity\Parts\Category;
use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use App\Entity\Parts\Storelocation; use App\Entity\Parts\Storelocation;
use App\Form\Type\StructuralEntityType;
use FOS\CKEditorBundle\Form\Type\CKEditorType; use FOS\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
@ -65,20 +66,20 @@ class PartType extends AbstractType
->add('description', TextType::class, ['required' => false, 'empty_data' => '', ->add('description', TextType::class, ['required' => false, 'empty_data' => '',
'label' => 'description.label', 'help' => 'bbcode.hint', 'attr' => ['placeholder' => 'part.description.placeholder'], 'label' => 'description.label', 'help' => 'bbcode.hint', 'attr' => ['placeholder' => 'part.description.placeholder'],
'disabled' => !$this->security->isGranted('description.edit', $part), 'empty_data' => '' ]) 'disabled' => !$this->security->isGranted('description.edit', $part), 'empty_data' => '' ])
->add('instock', IntegerType::class, /*->add('instock', IntegerType::class,
['attr' => ['min' => 0, 'placeholder' => 'part.instock.placeholder'], 'label' => 'instock.label', ['attr' => ['min' => 0, 'placeholder' => 'part.instock.placeholder'], 'label' => 'instock.label',
'disabled' => !$this->security->isGranted('instock.edit', $part), ]) 'disabled' => !$this->security->isGranted('instock.edit', $part), ]) */
->add('mininstock', IntegerType::class, ->add('mininstock', IntegerType::class,
['attr' => ['min' => 0, 'placeholder' => 'part.mininstock.placeholder'], 'label' => 'mininstock.label', ['attr' => ['min' => 0, 'placeholder' => 'part.mininstock.placeholder'], 'label' => 'mininstock.label',
'disabled' => !$this->security->isGranted('mininstock.edit', $part), ]) 'disabled' => !$this->security->isGranted('mininstock.edit', $part), ])
->add('category', EntityType::class, ['class' => Category::class, 'choice_label' => 'full_path', ->add('category', StructuralEntityType::class, ['class' => Category::class,
'attr' => ['class' => 'selectpicker', 'data-live-search' => true], 'label' => 'category.label', 'label' => 'category.label', 'disable_not_selectable' => true,
'disabled' => !$this->security->isGranted('move', $part), ]) 'disabled' => !$this->security->isGranted('move', $part), ])
->add('storelocation', EntityType::class, ['class' => Storelocation::class, 'choice_label' => 'full_path', /*->add('storelocation', EntityType::class, ['class' => Storelocation::class, 'choice_label' => 'full_path',
'attr' => ['class' => 'selectpicker', 'data-live-search' => true], 'required' => false, 'label' => 'storelocation.label', 'attr' => ['class' => 'selectpicker', 'data-live-search' => true], 'required' => false, 'label' => 'storelocation.label',
'disabled' => !$this->security->isGranted('storelocation.edit', $part), ]) 'disabled' => !$this->security->isGranted('storelocation.edit', $part), ]) */
->add('manufacturer', EntityType::class, ['class' => Manufacturer::class, 'choice_label' => 'full_path', ->add('manufacturer', StructuralEntityType::class, ['class' => Manufacturer::class,
'attr' => ['class' => 'selectpicker', 'data-live-search' => true], 'required' => false, 'label' => 'manufacturer.label', 'required' => false, 'label' => 'manufacturer.label', 'disable_not_selectable' => true,
'disabled' => !$this->security->isGranted('manufacturer.edit', $part), ]) 'disabled' => !$this->security->isGranted('manufacturer.edit', $part), ])
->add('manufacturer_product_url', UrlType::class, ['required' => false, 'empty_data' => '', ->add('manufacturer_product_url', UrlType::class, ['required' => false, 'empty_data' => '',
'label' => 'manufacturer_url.label', 'label' => 'manufacturer_url.label',

View file

@ -0,0 +1,145 @@
<?php
/**
*
* part-db version 0.1
* Copyright (C) 2005 Christoph Lechner
* http://www.cl-projects.de/
*
* part-db version 0.2+
* Copyright (C) 2009 K. Jacobs and others (see authors.php)
* http://code.google.com/p/part-db/
*
* Part-DB Version 0.4+
* Copyright (C) 2016 - 2019 Jan Böhmer
* https://github.com/jbtronics
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*
*/
namespace App\Form\Type;
use App\Entity\Base\StructuralDBElement;
use App\Entity\Parts\Storelocation;
use App\Repository\StructuralDBElementRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* This class provides a choice form type similar to EntityType, with the difference, that the tree structure
* of the StructuralDBElementRepository will be shown to user
* @package App\Form\Type
*/
class StructuralEntityType extends AbstractType
{
protected $em;
protected $options;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired(['class']);
$resolver->setDefaults(['attr' => ['class' => 'selectpicker', 'data-live-search' => true],
'show_fullpath_in_subtext' => true, //When this is enabled, the full path will be shown in subtext
'subentities_of' => null, //Only show entities with the given parent class
'disable_not_selectable' => false, //Disable entries with not selectable property
'choice_loader' => function (Options $options) {
return new CallbackChoiceLoader(function () use ($options) {
return $this->getEntries($options);
});
}, 'choice_label' => function ($choice, $key, $value) {
return $this->generateChoiceLabels($choice, $key, $value);
}, 'choice_attr' => function ($choice, $key, $value) {
return $this->generateChoiceAttr($choice, $key, $value);
}
]);
}
protected function generateChoiceAttr(StructuralDBElement $choice, $key, $value) : array
{
$tmp = array();
if ($this->options['show_fullpath_in_subtext'] && $choice->getParent() != null) {
$tmp += ['data-subtext' => $choice->getParent()->getFullPath()];
}
//Disable attribute if the choice is marked as not selectable
if ($this->options['disable_not_selectable'] && $choice->isNotSelectable()) {
$tmp += ['disabled' => 'disabled'];
}
return $tmp;
}
protected function generateChoiceLabels(StructuralDBElement $choice, $key, $value) : string
{
/** @var StructuralDBElement|null $parent */
$parent = $this->options['subentities_of'];
/*** @var StructuralDBElement $choice */
$level = $choice->getLevel();
//If our base entity is not the root level, we need to change the level, to get zero position
if ($this->options['subentities_of'] !== null) {
$level -= $parent->getLevel() - 1;
}
$tmp = str_repeat('&nbsp;&nbsp;&nbsp;', $choice->getLevel()); //Use 3 spaces for intendation
$tmp .= htmlspecialchars($choice->getName($parent));
return $tmp;
}
/**
* Gets the entries from database and return an array of them
* @param Options $options
* @return array
*/
public function getEntries(Options $options) : array
{
$this->options = $options;
/** @var StructuralDBElementRepository $repo */
$repo = $this->em->getRepository($options['class']);
$choices = $repo->toNodesList(null);
return $choices;
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
//Allow HTML in labels. You must override the 'choice_widget_options' block, so that can work
//See extendedBootstrap4_layout.html.twig for that...
$view->vars['use_html_in_labels'] = true;
parent::buildView($view, $form, $options); // TODO: Change the autogenerated stub
}
public function getParent()
{
return ChoiceType::class;
}
}

View file

@ -35,6 +35,7 @@ namespace App\Form;
use App\Entity\UserSystem\Group; use App\Entity\UserSystem\Group;
use App\Entity\Base\NamedDBElement; use App\Entity\Base\NamedDBElement;
use App\Entity\Base\StructuralDBElement; use App\Entity\Base\StructuralDBElement;
use App\Form\Type\StructuralEntityType;
use FOS\CKEditorBundle\Form\Type\CKEditorType; use FOS\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
@ -66,8 +67,8 @@ class UserAdminForm extends AbstractType
'attr' => ['placeholder' => 'user.username.placeholder'], 'attr' => ['placeholder' => 'user.username.placeholder'],
'disabled' => !$this->security->isGranted('edit_username', $entity), ]) 'disabled' => !$this->security->isGranted('edit_username', $entity), ])
->add('group', EntityType::class, ['class' => Group::class, 'choice_label' => 'name', ->add('group', StructuralEntityType::class, ['class' => Group::class,
'attr' => ['class' => 'selectpicker', 'data-live-search' => true], 'required' => false, 'label' => 'group.label', 'required' => false, 'label' => 'group.label', 'disable_not_selectable' => true,
'disabled' => !$this->security->isGranted('change_group', $entity), ]) 'disabled' => !$this->security->isGranted('change_group', $entity), ])
->add('first_name', TextType::class, ['empty_data' => '', 'label' => 'user.firstName.label', ->add('first_name', TextType::class, ['empty_data' => '', 'label' => 'user.firstName.label',

View file

@ -46,7 +46,27 @@ class StructuralDBElementRepository extends EntityRepository
*/ */
public function findRootNodes() : array public function findRootNodes() : array
{ {
return $this->findBy(['parent' => null]); return $this->findBy(['parent' => null], ['name' => 'ASC']);
}
/**
* Gets a flattened hierachical tree. Useful for generating option lists.
* @param StructuralDBElement|null $parent This entity will be used as root element. Set to null, to use global root
* @return StructuralDBElement[] A flattened list containing the tree elements.
*/
public function toNodesList(?StructuralDBElement $parent = null): array
{
$result = array();
$entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']);
foreach ($entities as $entity) {
/** @var StructuralDBElement $entity */
$result[] = $entity;
$result = array_merge($result, $this->toNodesList($entity));
}
return $result;
} }
} }

View file

@ -67,9 +67,11 @@
<div class="tab-pane active" id="home_common"> <div class="tab-pane active" id="home_common">
{{ form_row(form.name) }} {{ form_row(form.name) }}
{% if form.parent%} {% if form.parent%}
{{ form_row(form.parent) }} {{ form_row(form.parent) }}
{% endif %}
{% if form.not_selectable is defined %}
{{ form_row(form.not_selectable) }}
{% endif %} {% endif %}
{{ form_row(form.not_selectable) }}
{% block additional_controls %}{% endblock %} {% block additional_controls %}{% endblock %}

View file

@ -12,4 +12,21 @@
{% block form_group_class -%} {% block form_group_class -%}
col-9 col-9
{%- endblock form_group_class %} {%- endblock form_group_class %}
{%- block choice_widget_options -%}
{% for group_label, choice in options %}
{%- if choice is iterable -%}
<optgroup label="{{ choice_translation_domain is same as(false) ? group_label : group_label|trans({}, choice_translation_domain) }}">
{% set options = choice %}
{{- block('choice_widget_options') -}}
</optgroup>
{%- else -%}
{% if use_html_in_labels is defined and use_html_in_labels %}
<option value="{{ choice.value }}"{% if choice.attr %}{% with { attr: choice.attr } %}{{ block('attributes') }}{% endwith %}{% endif %}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice_translation_domain is same as(false) ? choice.label|raw : choice.label|trans({}, choice_translation_domain)|raw }}</option>
{% else %}
<option value="{{ choice.value }}"{% if choice.attr %}{% with { attr: choice.attr } %}{{ block('attributes') }}{% endwith %}{% endif %}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice_translation_domain is same as(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}</option>
{% endif %}
{%- endif -%}
{% endfor %}
{%- endblock choice_widget_options -%}